"Se um trabalhador quiser fazer bem o seu trabalho, ele deve primeiro afiar suas ferramentas." - Confúcio, "Os Analectos de Confúcio. Lu Linggong"
Primeira página > Programação > Como lidar com condições de corrida usando Java e PostgreSQL

Como lidar com condições de corrida usando Java e PostgreSQL

Publicado em 01/08/2024
Navegar:900

How to deal with race conditions using Java and PostgreSQL

Usando bloqueio para controlar a simultaneidade do banco de dados

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 {
    Optional stockByProductId = 
 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)
Optional findStockByProductIdWithLock(Long productId);
public void validateAndDecreaseSolution1(long productId, int quantity) {
    Optional stockByProductId = 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();

    Optional stockByProductId = 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.

Declaração de lançamento Este artigo está reproduzido em: https://dev.to/ramoncunha/how-to-deal-with-race-conditions-using-java-and-postgresql-4jk6?1 Caso haja alguma infração, entre em contato com study_golang@163 .com para excluí-lo
Tutorial mais recente Mais>

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