[Dúvida] Node.JS + Express - Melhor maneira de lidar com requisições duplicadas e simultâneas?
Olá devs :)
Senta que lá vem história haha
Talvez seja uma dúvida óbvia, mas apenas para contextualizar, a equipe em que eu trabalho esta migrando aos poucos de tecnologia, pegando novos projetos pequenos para ganharmos confiança e migrarmos totalmente assim que possível, nunca tabalhamos com elas antes, então estamos desenvolvendo juntamente com uma fase de aprendizado.
Estamos utilizando a seguinte stack para o backend:
Node.JS
+ Express
+ TypeScript
+ Prisma
+ PostgreSQL
Existem outras bibliotecas, mas como não estão envolvidas no problema, acredito que não seja necessário citar, além disso, estamos tentando aplicar boas práticas de padrão de projeto (SOLID, DDD e etc).
Contexto da funcionalidade:
O operador deve realizar a leitura do código de barra de um item, essa leitura cria o registro do item no local atual do operador (informado anteriormente) e indica onde o mesmo deve levar o item para ser armazenado, criando uma ordem de armazenagem (responsável por medir produtividade).
Durante a execução do meu serviço, eu verifico se o usuário já não realizou a leitura do item, caso sim, eu somente devolvo para o frontend a ordem de armazenagem que já foi criada anteriormente, evitando a duplicidade de saldo e ordens de armazenagem.
Fluxo super resumido realizado no sistema:
- Frontend dispara uma requisição POST informando o código do produto
- O Express "recebe" a REQUEST e chama a minha rota, vamos chama-la de /stock/read-item
- Minha rota chama meu CONTROLLER -> Extrai os dados da requisição necessário para chamar o serviço (Usuário logado, código do produto, etc).
- Meu CONTROLLER chama meu SERVICE, repassando as variaveis extraidas no passo anterior
- Meu SERVICE executa as regras de negócio baseado no contexto que expliquei acima e retorna para o CONTROLLER a ordem de armazenagem.
- CONTROLLER devolve a ordem de armazenagem na RESPONSE da requisição para que seja utilizada pelo frontend
Problema:
Por algum motivo, estou recebendo duas requisições referente a leitura do item em um período muito curto de tempo (realmente curto).
Requisição 1: 2023-12-08 14:07:42.130 Requisição 2: 2023-12-08 14:07:42.134 (00:00:00:004 milissegundos de diferença)
O problema, é que como ambas as requisições são praticamente simultâneas, a consulta no banco de dados para verificar se o item já foi lido anteriormente, também é simultânea, onde em ambas as consultas, é retornado para o serviço que o item ainda não foi lido.
Como o item "não foi lido", também não existe ordem de armazenagem e assim, eu chamo meu outro serviço responsável por criar o item e a ordem de armazenagem em ambas as requisições, duplicando o saldo :(
Realizei uma solução do jeitinho brasileiro, mas gostaria de saber, qual a solução de vocês para esses cenários? Não acredito que seja o usuário, então acredito que o navegador dele esta duplicando as requisições de alguma maneira.
Observação: Ocorreu 4x em 6 meses no ambiente produtivo, já foram lidos mais de 100 mil produtos, sei que não é muito, mas gostaria de tratar da maneira correta para que isso seja aplicado em todos os novos projetos :)
Agradeço quem leu até aqui, mesmo que seja de curiosidade para entender como realizar essa tratativa também no seu próprio projeto <3
@Edit
Obrigado a todos pelas sugestões.
Realizei a implementação com Redis e funcionou perfeitamente em nossas simulações, irei estudar se consigo fazer algo de maneira global agora :)
Obrigado especial ao @founty, @uriel e @tokyo por terem sugerido essa implementação.
O jeito que já vi pessoal resolvendo solicitações iguais é armazenando elas e respondendo.
Um tipo de cache. Como as solicitações são muito perto da outra. vc pode cachear as iguais e responder 1 só!
rapaz, acredito que o melhor seria um cache entre o banco e seu endpoint, ao salvar no banco(query demorada, talvez seja melhor ser feita em batches) voce também salva em um redis que é mais rapido, ou na memoria da propria máquina já que a diferença é curta.
depois, se voce recebe a segunda, voce confere no cache(ou memoria como dito), se nao existir você salva e, entao retorna para o usuario, a vantagem disso é voce poder definir o tempo do cache(ttl).
nao sei se seria o melhor, por causa do tempo muito curto entre as consultas, acredito que se voce salvar na maquina, na memoria do processo ja em execução seja mais rapido(exemplo, um Map), e procurar nele, e ao mesmo tempo enviar em batch pro banco, ou entao no map, depois no redis por segurança pra nao perder o cache e o banco, ou ate redis e banco somente, depende de como voce quer que seja implementado.
Essa requisição (POST) é feita automaticamente ou é disparada pelo usuário? Se for disparada por ele, ocorre em que momento? no clique de um botão ou ao fazer outra ação?
O que está sendo usando no front-end? Já vi coisas parecidas por mal uso dos states do React.
Não vale a pena avaliar com a equipe de front-end a possibilidade de ser um erro deles? Exemplo: uma state ou effect no React disparando duas vezes a requisição ao recarregar o componente.
Caso não, talvez seja interessante armazenar essas respostas do banco de dados em um banco não relacional para cache como o Redis.
No exemplo que tu citaste, já usou o inspetor de rede do navegador para verificar como essas duas requisições retornam? São realmente enviadas duas requisições para seu back-end? Qual payload elas carregam? É melhor procurar a origem do problema para montar uma solução personalizada no teu back-end.
Se aconteceu somente 4x em 6 meses, vai ser difícil rastrear esse problema. Talvez seja em algum serviço mais antigo e usado pouco? Ou um navegador específico? Talvez seja interessante começar a verificar o user-agent do cliente e mais informações do navegador e contexto a fim de rastrear com eficiência esse problema.
verifique se as chamadas simultâneas não estão sendo feitas de forma indevida e para resolver essa questão da leitura e escrita, você pode usar alguma condição a nivel de banco de dados que torne os dados únicos, dessa forma voce evita que os dados sejam inseridos de forma duplicada ou até mesmo incluir alguma estratégia de transaction para "lockar" o banco até que uma escrita seja finalizada logo a próxima leitura vai pegar os dados de forma integra e evitar essas duplicidades.