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

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

WBOY
WBOY원래의
2024-07-18 10:15:301033검색

How to deal with race conditions using Java and PostgreSQL

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

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

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

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

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

    if (stock <= 0 || possibleStock < 0) {
        throw new OutOfStockException("Out of stock");
    }

    stockRepository.decreaseStock(productId, quantity);
}

이 검증을 사용할 수 있지만 초당 수백, 수천, 수백만, 심지어 수십 개의 요청에 대해 이야기할 때 이 검증만으로는 충분하지 않습니다. 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<StockEntity> findStockByProductIdWithLock(Long productId);
public void validateAndDecreaseSolution1(long productId, int quantity) {
    Optional<StockEntity> 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<StockEntity> stockByProductId = stockRepository.findStockByProductId(productId);

    // check stock and throws exception if it is necessary

    stockRepository.decreaseStock(productId, quantity);
}

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

세 번째 솔루션 - WHERE 절

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

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




결론

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

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

전체 코드를 보고 싶으시다면 제 GitHub에서 확인하실 수 있습니다.

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

위 내용은 Java 및 PostgreSQL을 사용하여 경쟁 조건을 처리하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.