[Spring] 동시성 이슈 해결2: Database💽

🙈·2024년 1월 23일
0

Database의 Lock을 통해 해결

🔒 다양한 lock의 종류

  1. Pessimistic lock (비관적 락)

    • 실제로 데이터에 Lock을 걸어 정합성을 맞추는 방법이다.
    • Exclusive lock(베타적 잠금)이 걸리면 다른 트랜잭션에서는 lock이 해제되기 전까지 데이터를 가져갈 수 없다.
    • 다만, 데드락이 걸리는 상황에 주의해서 사용해야한다.

    ⇒ 자원을 요청하면 동시성 문제가 발생할 것을 예상하고 lock을 걸어버리는 비관적 락이다.

  2. Optimistic lock (낙관적 락)

    • 버전을 통해 정합성을 맞추는 방법이다.
    • 데이터를 읽은 후에 update 를 수행할 때 현재 내가 읽은 버전이 맞는지 확인하며 업데이트를 한다.
    • 읽은 버전에서 수정사항이 생겼을 경우에는 application에서 다시 읽은 후에 작업을 수행해야 한다.

    ⇒ 자원에 락을 걸어 선점하기보단, 동시성 문제가 발생했을 때 감지하고 처리하는 낙관적 락이다.

  3. Named lock

    • 이름을 가진 metadata locking이다.
    • 이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션은 이 lock을 획득할 수 없다.
    • transaction이 종료될 때 lock이 자동으로 해지되지 않아 별도의 명령어로 해제하거나 선점 시간이 끝나야 해제된다.

😒 Pessimistic lock 활용

적용 과정

Spring data jpa를 활용하면 @Lock을 통해 손쉽게 pessimistice lock을 구현할 수 있다.

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

결과 확인

Test가 성공적으로 실행되며, 실행 중 for update라는 부분이 lock을 걸고 데이터를 가져오는 부분이다.

장·단점

장점

  • 충돌이 빈번한 경우 롤백의 횟수를 줄일 수 있고, Optimistic lock보다 성능이 좋을 수 있다.
  • lock을 통해 업데이터를 제어하므로 데이터의 정합성이 보장된다.

단점

  • 별도로 lock을 걸어야 하므로 성능 감소가 있을 수 있다.
  • 서로의 자원이 필요한 경우, 데드락이 발생할 수 있다.

🌞 Optimistic lock 활용

적용 과정

  1. Optimistic lock을 활용하기 위해 Stock Entityversion이라는 attribute을 추가한다.

    @Version
    private Long version;
  2. Spring Data JPA에서 제공하는 @Lock을 통해 Optimistic lock을 구현한다.

    @Lock(LockModeType.OPTIMISTIC)
    @Query("select s from Stock s where s.id = :id")
    Stock findByIdWithOptimisticLock(Long id);
  3. Optimistic lock은 실패했을 때 재시도 과정이 필요하므로 facade를 만들어 그곳에서 service layer의 함수를 호출한다.

    @Component
    public class OptimisticLockFacade {
    
      private final OptimisticLockStockService optimisticLockStockService;
      public OptimisticLockFacade(OptimisticLockStockService optimisticLockStockService) {
          this.optimisticLockStockService = optimisticLockStockService;
      }
    
      public void decrease(Long id, Long quantity) throws InterruptedException {
          while (true) {
              try {
                  optimisticLockStockService.decrease(id, quantity);
    
                  break;
              } catch (Exception e) {
                  Thread.sleep(50);
              }
          }
       }
    }

장·단점

장점

  • 충돌이 적게 발생할 때, 별도의 lock을 잡지 않으므로 pessimistic lock보다 성능이 좋다.

단점

  • update에 실패했을 때의 재시도 로직을 개발자가 직접 작성해야 한다.

⚖️ Pessimistic lock vs. Optimistic lock

따라서 충돌의 발생 빈도에 따라 lock을 다르게 사용하는 것을 추천한다.

  • 충돌이 빈번한 경우 ⇒ Pessimistic lock
  • 충돌이 적은 경우 ⇒ Optimistic lock

🙋🏻 Named lock 활용

Named Lock은 이름을 가진 metadata lock으로 이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션은 이 락을 획득할 수 없다.

트랜잭션이 종료될 때 lock이 자동으로 해제되지 않기 때문에 다음과 같은 상황에서 lock을 해제할 수 있다.

  • 별도의 명령어 사용
  • 선점 시간이 종료

MySQL에서의 Named lock 사용 명령어는 다음과 같다.

  • lock 획득: get-lock
  • lock 해제: release-lock

강의 예제 vs. 실무

강의에서는 편의를 위해 JPA의 Native Query를 사용하고 동일한 data source를 사용한다.

실제로 사용할 때에는 data source를 분리하는 것을 추천한다.
같은 데이터 소스를 사용하면 connection pool이 부족해져 다른 서비스에도 영향을 줄 수 있다.

적용 과정

  1. lock을 획득하고 종료하는 LockRepository를 정의한다.

    public interface LockRepository extends JpaRepository<Stock, Long> {
      @Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
      void getLock(String key);
    
      @Query(value = "select release_lock(:key)", nativeQuery = true)
      void releaseLock(String key);
    }
  2. StockService 로직을 활용하여 락을 획득하고 해제할 facade를 정의한다.

    try {
      lockRepository.getLock(id.toString());
      stockService.decreaseStock(id, quantity);
    } finally {
      lockRepository.releaseLock(id.toString());
    }
  3. StockService의 재고 감소 로직은 facade의 transaction과 별도로 실행되어야 하므로 propagation을 변경한다.

    // 부모(NamedLockStockFacade)의 transaction과 별도로 실행되어야 하므로 propagation 변경
    @Transactional(propagation = Propagation.REQUIRES_NEW) 
    public void decreaseStock(Long id, Long quantity) {
      // Stock 조회
      Stock stock = stockRepository.findById(id).orElseThrow();
      stock.decrease(quantity);
      stockRepository.saveAndFlush(stock);
    }
  4. 예제에서는 같은 data source를 활용하여 두 로직(재고 감소 & lock의 획득 및 해제)을 실행하므로 connection pool의 사이즈를 증가시킨다.

    // application.yml 파일
    spring: 
    datasource:
      hikari:
        maximum-pool-size: 40

실무에서의 활용 방안과 장·단점

Named lock은 주로 Distributed lock을 구현할 때에 사용된다.

장점

  • 타임아웃을 구현하기 어려운 Pessimistic lock과 달리 Named lock을 타임아웃을 구현하기 쉽다.

단점

  • 트랜잭션 종료 시에 락 해제, 세션 관리에 주의해야하기에 구현이 복잡할 수 있다.
profile
개발 일기🌱

0개의 댓글