Imagine que você está trabalhando em um sistema de comércio eletrônico e milhares de pessoas tentam comprar o último produto restante ao mesmo tempo. Porém, muitos deles poderiam prosseguir para o checkout e finalizar o pedido. Ao verificar seu estoque, você tem um produto com quantidade negativa. Como isso foi possível e como você pode resolver isso?
Vamos codificar! A primeira coisa que você pode pensar é verificar o estoque antes de finalizar a compra. Talvez algo assim:
public void validateAndDecreaseSolution(long productId, int quantity { OptionalstockByProductId = stockRepository.findStockByProductId(productId); int stock = stockByProductId.orElseThrow().getStock(); int possibleStock = stock - quantity; if (stock Você pode usar essa validação, mas quando falamos de centenas, milhares, milhões ou até dezenas de solicitações por segundo, essa validação não será suficiente. Quando 10 solicitações atingirem esse trecho de código exatamente ao mesmo tempo e o banco de dados retornar o mesmo valor para stockByProductId, seu código será quebrado. Você precisa bloquear outras solicitações enquanto fazemos essa verificação.
Primeira solução - PARA ATUALIZAÇÃO
Adicione uma instrução lock em seu SELECT. Neste exemplo fiz isso usando FOR UPDATE com Spring Data. Como diz a documentação do PostgreSQL
FOR UPDATE faz com que as linhas recuperadas pela instrução SELECT sejam bloqueadas como se estivessem para atualização. Isso evita que eles sejam modificados ou excluídos por outras transações até que a transação atual termine.
@Query(value = "SELECT * FROM stocks s WHERE s.product_id = ?1 FOR UPDATE", nativeQuery = true) OptionalfindStockByProductIdWithLock(Long productId); public void validateAndDecreaseSolution1(long productId, int quantity) { OptionalstockByProductId = stockRepository.findStockByProductIdWithLock(productId); // ... validate stockRepository.decreaseStock(productId, quantity); } Todas as solicitações à tabela de estoques usando o ID do produto aguardarão até que a transação real seja concluída. O objetivo aqui é garantir que você obtenha o último valor atualizado do estoque.
Segunda solução – pg_advisory_xact_lock
Esta solução é semelhante à anterior, mas você pode selecionar qual é a chave de bloqueio. Bloquearemos toda a transação até finalizarmos todo o processamento de validação e redução de estoque.
public void acquireLockAndDecreaseSolution2(long productId, int quantity) { Query nativeQuery = entityManager.createNativeQuery("select pg_advisory_xact_lock(:lockId)"); nativeQuery.setParameter("lockId", productId); nativeQuery.getSingleResult(); OptionalstockByProductId = stockRepository.findStockByProductId(productId); // check stock and throws exception if it is necessary stockRepository.decreaseStock(productId, quantity); } A próxima solicitação só interagirá com um produto com o mesmo ID após o término desta transação.
Terceira solução - cláusula WHERE
Nesse caso, não bloquearemos nossa linha ou transação. Vamos permitir que esta transação continue até a declaração de atualização. Observe a última condição: estoque > 0. Isso não permitirá que nosso estoque seja menor que zero. Portanto, se duas pessoas tentarem comprar ao mesmo tempo, uma delas receberá um erro porque nosso banco de dados não permitirá estoque
@Transactional @Modifying @Query(nativeQuery = true, value = "UPDATE stocks SET stock = stock - :quantity WHERE product_id = :productId AND stock > 0") int decreaseStockWhereQuantityGreaterThanZero(@Param("productId") Long productId, @Param("quantity") int quantity);Conclusão
A primeira e a segunda soluções usam o bloqueio pessimista como estratégia. O terceiro é o bloqueio otimista. A estratégia de bloqueio pessimista é usada quando você deseja acesso restritivo a um recurso enquanto executa qualquer tarefa que envolva esse recurso. O recurso de destino ficará bloqueado para qualquer outro acesso até você finalizar o processo. Tenha cuidado com impasses!
Com o bloqueio otimista, você pode realizar várias consultas no mesmo recurso sem nenhum bloqueio. É usado quando é improvável que ocorram conflitos. Normalmente, você terá uma versão relacionada à sua linha e, ao atualizar essa linha, o banco de dados comparará sua versão de linha com a versão de linha no banco de dados. Se ambos forem iguais, a mudança será bem-sucedida. Caso contrário, você deverá tentar novamente. Como você pode ver, não uso nenhuma linha de versão neste artigo, mas minha terceira solução não bloqueia nenhuma solicitação e controla a simultaneidade usando a condição stock > 0.
Se quiser ver o código completo, você pode conferir no meu GitHub.
Existem muitas outras estratégias para implementar bloqueios pessimistas e otimistas, você pode pesquisar mais sobre FOR UPDATE WITH SKIP LOCKED por exemplo.
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3