ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OptimisticLock / Pessimistic Lock / Named Lock
    BackEnd/실습 2022. 9. 26. 15:41

     

    DB 를 통해 동시성을 제어할 때는 Pessimistic Lock, Optimistic Lock, Named Lock 세가지 방법이 있다.

    이미 mysql 사용한다면 별도의 비용 없이 동시성을 제어할 수 있고, 어느 정도 트래픽까지 문제없이 사용할 수 있다.
    더 좋은 성능이 필요하다면 redis 를 사용해 동시성 제어가 가능하다.


    Pessimistic lock 은 미리 동시에 접근한다고 가정하고 방어하기 때문에 충돌이 잦은 경우에, 

    Optimistic lock 은 update 하는 시점에 select 해온 버전에서 데이터가 변경이 없는지 확인하고 update 를 하는데, 충돌이 거의 일어나지 않는 상황에 사용하기 적절하다. Optimistic Lock 은 버전이 달라졌을 경우 재시도하는 로직을 함께 구현해줘야한다.

     

    JPA 를 사용할 때 Optimistic과 pessimistic 락의 구현이 쉽다. 

    엔티티에 @Version 컬럼만 추가해주면 쿼리에 자동으로 version 이 조건으로 들어간 쿼리가 생성된다.

     

    1. Pessimistic Lock 과 Optimistic Lock 모두 구현은 비슷하다. @Lock 어노테이션 내 모드만 다르게 한다.

    public interface StockRepository extends JpaRepository<Stock,Long> {
    
        @Lock(value = LockModeType.PESSIMISTIC_WRITE)
        @Query("select s from Stock s where s.id=:id")
        Stock findByIdWithPessimisticLock(@Param("id") Long id);
    
        @Lock(value = LockModeType.OPTIMISTIC)
        @Query("select s from Stock s where s.id=:id")
        Stock findByIdWithOptimisticLock(@Param("id") Long id);
    
    }
    

    2. Optimistic Lock에는 버전이 필요하므로 엔티티에 아래와 같이 필드를 추가해준다.

    @Version
    private Long version;
    
    update stock set product_id=?, quantity=?, version=? where id=? and version=?

    Named Lock 은 이름을 가진 Metadata에 대한 Lock인데, 이름을 가진 Lock을 획득하여 해제할 때까지 다른 세션은 해당 Lock을 획득할 수 없다. Transaction이 종료될 때 Lock이 자동으로 해제되지 않으므로 별도의 명령어를 사용하거나 선점시간이 종료되어 수동 해제처리를 해줘야 한다는 주의점이 있다.

    Named Lock을 활용할 때 데이터소스를 분리하지않고 하나로 사용하게되면 커넥션풀이 부족해지는 현상을 겪을 수 있어서 락을 사용하지 않는 다른 서비스까지 영향을 끼칠 수 있다는 것이다.
    Named Lock을 활용하면 분산 락을 구현할 수 있다.

    Pessmistic Lock은 타임아웃을 구현하기 쉽지만 Named Lock은 타임아웃을 구현하기 쉽다. 

    데이터 정합성을 받춰야 하는 경우에도 Named Lock이 좋다.

    하지만 트랜잭션 종료 시에 Lock 해제와 세션 관리 (데이터 소스 분리 시) 관리가 수동으로 진행되어야 하고,

    일일이 수동으로 해야 한다는 불편한 점이 있어 실무 구현에서는 좀 빡세다.

    참고로, Pessmistic Lock은 column/row 단계에서 Lock을 걸지만, Named Lock은 metadata 단위에 lock을 건다. 또한, Named Lock에서는 Thread가 아니라 Session이라고 부른다.

    public interface LockRepository extends JpaRepository<Stock, Long> {
    
        @Query(value="select get_lock(:key,3000)", nativeQuery = true)
        void lock(@Param("key") String key);
    
        @Query(value="select release_lock(:key)", nativeQuery = true)
        void releaseLock(@Param("key") String key);
    }
    

     

    lock 을 관리할 때 하나의 트랜잭션으로 묶어줘야 lock 의 생성과 해제가 같은 커넥션에서 이뤄진다.

    @Transactional
    public void decrease(Long id, Long quantity){
        try{
            lockRepository.lock(id.toString());
            stockService.decrease(id,quantity);
        } finally {
            lockRepository.releaseLock(id.toString());
        }
    }

    2. Propagation 수준을 Requires_New 로 설정해 서비스 실행은 별도의 트랜잭션으로 분리한다.

    부모 트랜잭션에서 lock 의 해제가 자식 트랜잭션의 commit 이후 수행되도록 하는 것이다.

    @RequiredArgsConstructor
    @Service
    public class StockService {
        private final StockRepository stockRepository;
    
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public  void decrease(Long id, Long quantity){
            Stock stock = stockRepository.findById(id).orElseThrow();
            stock.decrease(quantity);
            stockRepository.saveAndFlush(stock);
        }
    }

     




    반응형
Designed by Tistory.