강의
[재고시스템으로 알아보는 동시성 이슈 해결방법] #4 Pessimistic Lock 비관적 락 , Optimistic Lock 낙관적 락, Named Lock 2
dev_rosieposie
2023. 11. 9. 20:55
들어가기 전..
1. 이전 게시글과 이어지는 게시글이므로 프로젝트 환경 및 세팅은 아래 링크에서 확인해주세요!
2. 전체 코드는 아래의 Git에서 확인 가능합니다.
[재고시스템으로 알아보는 동시성 이슈 해결방법] #4 Pessimistic Lock 비관적 락 , Optimistic Lock 낙관적
들어가기 전... 1. 이전 게시글과 이어지는 게시글이므로 프로젝트 환경 및 세팅은 아래 링크에서 확인해주세요! 2. 전체 코드는 아래의 Git에서 확인 가능합니다. [재고시스템으로 알아보는 동시
dev-rosiepoise.tistory.com
GitHub - dev-rosieposie128/stock: 재고시스템으로 알아보는 동시성이슈 해결방법
재고시스템으로 알아보는 동시성이슈 해결방법. Contribute to dev-rosieposie128/stock development by creating an account on GitHub.
github.com
지난 게시글에서는 MySQL이 지원하는 다양한 Lock의 개념을 알아보고 그 차이를 알아보았다.
이번에는 어떻게 해당 Lock을 사용하여 해결하는지 확인해보고 특징을 알아보자.
목표
- Pessimistic Lock을 활용한 개발
- Optimistic Lock을 활용한 개발
- Named Lock을 활용한 개발
Pessimistic Lock을 활용해보기
- Thread1이 락을 걸고 데이터를 가져간다
- 그 후 Thread2가 락을 걸고 데이터 획득을 시도한다
- Thread1이 이미 점유중이므로 잠시 대기하게 된다
- Thread1의 작업이 모두 완료가 되면 Thread2가 락을 걸고 데이터를 가져갈 수 있게된다
StockRepository class
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPerssimisticLock(Long id);
}
- lock을 걸고 데이터를 가져오는 메소드
- native query를 활용해서 query 작성
- spring data jpa에서는 lock이라는 어노테이션을 통해 손쉽게 pessimistic lock을 구현할 수 있다
PessimisticLockStockService class
@Service
public class PessimistickLockStockService {
private StockRepository stockRepository;
public PessimistickLockStockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity){
Stock stock = stockRepository.findByIdWithPerssimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
}
기존 StockApplicationTests에서 서비스만 변경해주고 실행
@Autowired
private PessimistickLockStockService stockService;
=> 성공
- for update 부분이 락을 걸고 데이터를 가져오는 부분
장단점
- 충돌이 자주 일어난다면, optimistic lock(낙관적 락)보다 성능이 좋을 수 있음
- 락을 통해 업데이트를 제어하기 때문에 데이터 정합성이 보장된다.
- 별도의 락을 잡기 때문에 성능감소가 있을 수 있다.
Optimistic Lock을 활용해보기
- 서버1과 서버2가 버전1인 데이터를 가져간다
- 그리고 서버1이 업데이트할 때 버전1을 올려준다
- 그 후에 서버가 버전이 1인 조건을 가지고 업데이트를 시도한다
- 하지만 현재 데이터의 버전은 2이므로 업데이트를 실패한다
이것처럼 내가 읽은 버전에서 수정사항이 생겼을 경우 어플리케이션에서 다시 읽은 후에 작업을 수행해야 한다
Stock class
@Entity
public class Stock {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private Long quantity;
@Version
private Long version;
public Stock() {
}
public Stock(Long productId, Long quantity) {
this.productId = productId;
this.quantity = quantity;
}
public Long getQuantity() {
return quantity;
}
public void decrease(Long quantity){
if(this.quantity - quantity < 0){
throw new RuntimeException("0개 미만");
}
this.quantity = this.quantity-quantity;
}
}
- optimistic lock을 사용하기 위해 버전 컬럼을 추가해야한다.
- @Version - java.x.persistence 패키지
StockRepository class
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(Long id);
}
- optimistic lock을 사용하기 위한 쿼리 작성
OptimisticLockStockService class
@Service
public class OptimistickLockStockService {
private StockRepository stockRepository;
public OptimistickLockStockService(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
@Transactional
public void decrease(Long id, Long quantity){
Stock stock = stockRepository.findByIdWithPerssimisticLock(id);
stock.decrease(quantity);
stockRepository.save(stock);
}
}
OptimisticLockStockFacade class
- 실패했을 때 재시도를 해야하므로 while문을 사용한다
- 수량감소에 실패하게 되면 50ms 후 재시도 한다
- 정상적으로 업데이트가 된 경우 break를 통해 빠져나오게 한다
OptimisticLockStockFacadeTest
@SpringBootTest
class OptimisticLockStockFacadeTest {
@Autowired
private OptimisticLockStockFacade optimisticLockStockFacade;
@Autowired
private StockRepository stockRepository;
@BeforeEach
public void before(){
stockRepository.saveAndFlush(new Stock(1L,100L));
}
@AfterEach
public void after(){
stockRepository.deleteAll();
}
@Test
public void 동시에_100개의_요청() throws InterruptedException {
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);
CountDownLatch latch = new CountDownLatch(threadCount);
for(int i=0; i< threadCount; i++){
executorService.submit(()->{
try{
try {
optimisticLockStockFacade.decrease(1L, 1L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}finally {
latch.countDown();
}
});
}
latch.await();
Stock stock = stockRepository.findById(1L).orElseThrow();
// 100 - (1*00) = 0 : 예상 값
assertEquals(0, stock.getQuantity());
}
}
- OptimisticLockStockFacade를 생성자 주입을 받는다
결과
=> 성공
장단점
- 별도의 락을 잡지 않으므로 pessimistic lock 비관적락보다 성능상 이점이 있다
- 단점으로는 업데이트가 실패했을 때 재시도를 하는 로직을 직접 작성해야 한다
- 충돌이 빈번하게 나타날 것으로 예상된다면 pessimistic lock 사용을, 빈번하게 나타나지 않을 것으로 예상된다면 optimistic lock사용을 권장한다.