락을 이용한 동시성 제어에서 트랜잭션 전파 속성 주의점

박진형·2022년 11월 14일
1

레디스를 이용한 스핀락을 사용시 트랜잭션 전파 속성에 관련해 주의해야할 점을 나름대로 정리했습니다.

레디스를 이용한 락을 사용할 때 주의점

상위 레벨과 하위 레벨의 트랜잭션을 별개로 가져가야한다.

→ 락을 거는 트랜잭션과 비즈니스 로직을 수행하는 트랜잭션을 분리해야한다.

@Transactional
public void decrease(Long key, Long quantity)throwsInterruptedException {
while(!redisLockRepository.lock(key)) {
      Thread.sleep(100);
   }

try{
      stockService.decrease(key, quantity);
   }finally{
      redisLockRepository.unlock(key);
   }
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decrease(Long id, Long quantity) {
   Stock stock = stockRepository.findById(id).orElseThrow();

   stock.decrease(quantity);

   stockRepository.saveAndFlush(stock);
}

이유 - 하나의 락을 거는 트랜잭션과 비즈니스 로직의 트랜잭션을 같은 트랜잭션으로 사용할 경우 commit이 되기전에 unlock을 수행할 수 있고, unlock 과 commit 사이의 시점에 또 다른 요청이 들어올 때 커밋이 되기 전이므로 정합성 문제가 발생할 수 있습니다.


그림으로 간단히 나타내본 예시입니다.

n = 100이고 1씩 줄이는 비즈니스로직을 수행한다고 했을 때,

하나의 트랜잭션을 사용할 경우 실제 커밋되는 시점은 비즈니스 로직 수행 → 언락 → 커밋 순서입니다.

언락 → 커밋 사이의 시점에는 Request A에서는 n = 99고 아직 커밋되기 전입니다.

그 시점에서 Request B가 lock → decrease를 수행한다면 Request B에서도 최종적으로 n = 99입니다.

하지만 트랜잭션 전파 속성을 조정한다면 이 문제를 해결할 수 있습니다.


트랜잭션을 분리해야 한다.

@Transactional(propagation = Propagation.REQUIRES_NEW)

스프링의 기본 트랜잭션 전파 범위는 REQUIRED 입니다.

REQUIRED는 이미 시작된 트랜잭션이 있다면 같은 트랜잭션을 사용하고, 없다면 새로운 트랜잭션을 시작합니다.

하지만 REQUIRES_NEW는 항상 새로운 트랜잭션을 시작합니다. 이미 진행중인 트랜잭션은 보류하고 하위 트랜잭션이 마무리 된 후에 상위 트랜잭션을 마무리 합니다.

REQUIRES_NEW를 사용한 로직 수행 과정을 그림으로 살펴보겠습니다.

아까와는 다르게 decrease를 수행하는 비즈니스 로직 부분에서는 새로운 트랜잭션인 트랜잭션 B를 사용합니다.

이 트랜잭션이 마무리(Commit) 되고 난 후 unlock을 수행하므로 새로운 다른 요청에서 lock을 획득한 시점에서는 이미 기존의 요청의 비즈니스 로직에 의해 decrease가 성공적으로 commit된 시점입니다.

결과적으로 전파 속성을 REQUIRES_NEW로 설정을 해 락 레벨과 비즈니스 로직 레벨의 트랜잭션을 나누어 Request B가 lock을 획득한 시점에 Request A의 비즈니스 로직이 commit된 이후임을 보장하게 된 것 입니다.

0개의 댓글