강의

[재고시스템으로 알아보는 동시성 이슈 해결방법] #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을 활용해보기

  1. Thread1이 락을 걸고 데이터를 가져간다
  2. 그 후 Thread2가 락을 걸고 데이터 획득을 시도한다
  3. Thread1이 이미 점유중이므로 잠시 대기하게 된다
  4. 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. 서버1과 서버2가 버전1인 데이터를 가져간다
  2. 그리고 서버1이 업데이트할 때 버전1을 올려준다
  3. 그 후에 서버가 버전이 1인 조건을 가지고 업데이트를 시도한다
  4. 하지만 현재 데이터의 버전은 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사용을 권장한다.