당신이 전자상거래 시스템을 개발하고 있는데 수천 명의 사람들이 동시에 마지막 남은 제품을 구매하려고 한다고 상상해 보십시오. 그러나 이들 중 다수는 결제 단계로 이동하여 주문을 완료할 수 있었습니다. 재고를 확인해보니 제품 수량이 마이너스입니다. 어떻게 이런 일이 가능했으며 어떻게 해결할 수 있나요?
코딩하자! 가장 먼저 생각할 수 있는 것은 결제 전 재고를 확인하는 것입니다. 어쩌면 다음과 같을 수도 있습니다:
public void validateAndDecreaseSolution(long productId, int quantity { OptionalstockByProductId = 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) OptionalfindStockByProductIdWithLock(Long productId); public void validateAndDecreaseSolution1(long productId, int quantity) { OptionalstockByProductId = 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(); OptionalstockByProductId = 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에 대해 자세히 검색할 수 있습니다.
부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.
Copyright© 2022 湘ICP备2022001581号-3