"일꾼이 일을 잘하려면 먼저 도구를 갈고 닦아야 한다." - 공자, 『논어』.
첫 장 > 프로그램 작성 > Java 및 PostgreSQL을 사용하여 경쟁 조건을 처리하는 방법

Java 및 PostgreSQL을 사용하여 경쟁 조건을 처리하는 방법

2024-08-01에 게시됨
검색:607

How to deal with race conditions using Java and PostgreSQL

잠금을 사용하여 데이터베이스 동시성 제어

당신이 전자상거래 시스템을 개발하고 있는데 수천 명의 사람들이 동시에 마지막 남은 제품을 구매하려고 한다고 상상해 보십시오. 그러나 이들 중 다수는 결제 단계로 이동하여 주문을 완료할 수 있었습니다. 재고를 확인해보니 제품 수량이 마이너스입니다. 어떻게 이런 일이 가능했으며 어떻게 해결할 수 있나요?

코딩하자! 가장 먼저 생각할 수 있는 것은 결제 전 재고를 확인하는 것입니다. 어쩌면 다음과 같을 수도 있습니다:

public void validateAndDecreaseSolution(long productId, int quantity {
    Optional stockByProductId = 
 stockRepository.findStockByProductId(productId);

    int stock = stockByProductId.orElseThrow().getStock();
    int possibleStock = stock - quantity;

    if (stock 



이 검증을 사용할 수 있지만 초당 수백, 수천, 수백만, 심지어 수십 개의 요청에 대해 이야기할 때 이 검증만으로는 충분하지 않습니다. 10개의 요청이 정확히 동시에 이 코드 조각에 도달하고 데이터베이스가 stockByProductId에 대해 동일한 값을 반환하면 코드가 중단됩니다. 이 확인을 수행하는 동안 다른 요청을 차단할 수 있는 방법이 필요합니다.

첫 번째 솔루션 - 업데이트용

SELECT에 잠금 문을 추가하세요. 이 예에서는 Spring Data와 함께 FOR UPDATE를 사용하여 이 작업을 수행했습니다. PostgreSQL 문서에 따르면

FOR UPDATE를 사용하면 SELECT 문으로 검색된 행이 업데이트되는 것처럼 잠깁니다. 이는 현재 트랜잭션이 종료될 때까지 다른 트랜잭션에 의해 수정되거나 삭제되는 것을 방지합니다.

@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);
}

제품 ID를 사용하는 재고 테이블에 대한 모든 요청은 실제 거래가 완료될 때까지 기다립니다. 여기서의 목표는 주식의 마지막 업데이트 가치를 확인하는 것입니다.

두 번째 해결책 - pg_advisory_xact_lock

이 솔루션은 이전 솔루션과 유사하지만 잠금 키가 무엇인지 선택할 수 있습니다. 검증 및 재고 감소 처리가 모두 완료될 때까지 전체 거래를 잠글 예정입니다.

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);
}

다음 요청은 이 거래가 종료된 후 동일한 ID를 가진 제품과만 상호 작용합니다.

세 번째 솔루션 - WHERE 절

이 경우 행이나 거래를 잠그지 않습니다. 업데이트 문이 나올 때까지 이 트랜잭션이 계속되도록 허용하겠습니다. 마지막 조건에 주목하세요: 재고 > 0. 이는 재고가 0보다 작은 것을 허용하지 않습니다. 따라서 두 사람이 동시에 구매하려고 하면 데이터베이스에서 재고

@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);

결론

첫 번째와 두 번째 솔루션은 비관적 잠금을 전략으로 사용합니다. 세 번째는 낙관적 잠금입니다. 비관적 잠금 전략은 해당 리소스와 관련된 작업을 수행하는 동안 해당 리소스에 대한 제한적인 액세스를 원할 때 사용됩니다. 프로세스가 완료될 때까지 대상 리소스는 다른 액세스를 위해 잠겨 있습니다. 교착상태에 주의하세요!

낙관적 잠금을 사용하면 블록 없이 동일한 리소스에 대해 다양한 쿼리를 수행할 수 있습니다. 충돌이 일어날 가능성이 없을 때 사용됩니다. 일반적으로 행과 관련된 버전이 있으며 이 행을 업데이트하면 데이터베이스는 행 버전을 데이터베이스의 행 버전과 비교합니다. 둘 다 동일하면 변경이 성공합니다. 그렇지 않은 경우 다시 시도해야 합니다. 보시다시피 이 문서에서는 버전 행을 사용하지 않지만 세 번째 솔루션은 요청을 차단하지 않고 재고 > 0 조건을 사용하여 동시성을 제어합니다.

전체 코드를 보려면 내 GitHub에서 확인하세요.

비관적 잠금과 낙관적 잠금을 구현하는 다른 전략이 많이 있습니다. 예를 들어 FOR UPDATE WITH SKIP LOCKED에 대해 자세히 검색할 수 있습니다.

릴리스 선언문 이 기사는 https://dev.to/ramoncunha/how-to-deal-with-race-conditions-using-java-and-postgresql-4jk6?1에 복제되어 있습니다. 침해가 있는 경우에는 Study_golang@163으로 문의하시기 바랍니다. .com에서 삭제하세요
최신 튜토리얼 더>

부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.

Copyright© 2022 湘ICP备2022001581号-3