당신이 전자상거래 시스템을 운영하고 있는데 수천 명의 사람들이 동시에 마지막 남은 제품을 구매하려고 한다고 상상해 보세요. 그러나 그들 중 다수는 결제를 진행하고 주문을 완료할 수 있었습니다. 재고를 확인해보니 제품 수량이 마이너스입니다. 어떻게 이런 일이 가능했고, 어떻게 해결할 수 있나요?
코딩하자! 가장 먼저 생각할 수 있는 것은 결제 전 재고를 확인하는 것입니다. 어쩌면 다음과 같을 수도 있습니다:
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를 사용하여 재고 테이블에 대한 모든 요청은 실제 거래가 완료될 때까지 기다립니다. 여기서의 목표는 주식의 마지막 업데이트 가치를 확인하는 것입니다.
이 솔루션은 이전 솔루션과 유사하지만 잠금 키가 무엇인지 선택할 수 있습니다. 검증 및 재고 감소 처리가 모두 완료될 때까지 전체 거래를 잠그겠습니다.
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를 가진 제품과만 상호 작용합니다.
이 경우 행이나 거래를 잠그지 않습니다. 업데이트 문이 나올 때까지 이 트랜잭션이 계속되도록 허용하겠습니다. 마지막 조건에 주목하세요: 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!