이전 블로그에서 동시성 문제를 해결하기 위해Lettuce를 프로젝트에 도입하였습니다. 저는 Lettuce와 Redisson을 둘 다 사용하려고 합니다.
재시도가 필요하지 않은 Lock은 Lettuce를 활용할 것이고 재시도가 필요한 경우에는 Redisson을 사용할 것 입니다. setnx 메소드는 만약 키가 존재하지 않는다면 설정하게 되는 것이므로 Redis 에 계속해서 LockKeyName 이 존재하는지 확인해야만 합니다. 따라서 순회하는 동안 계속해서 Redis 에 요청을 보내게 되는 것이므로 스레드 혹은 프로세스가 많다면 Redis 에 부하가 가게 될 것이기 때문에 재시도가 필요한 경우에는 Redisson 방식을 사용할 것입니다.
pub-sub 방식을 사용해보기 위해서는 2개의 redis cli가 필요합니다(즉, 2개의 터미널이 필요합니다).
dependencies {
implementation 'org.redisson:redisson-spring-boot-starter:3.17.6'
}
Lettuce 와 다르게 Redisson 은 계속 락 획득을 계속 시도하는게 아니기 때문에 Redis의 부하를 줄일 수 있습니다.
RLock
redisson에는 RLock이라는 객체가 존재합니다. 이 객체를 통해 락을 컨트롤할 수 있습니다. RLock을 얻기 위해서는 RedissonClient.getLock() 메서드를 호출해주어야 합니다.
Redisson 같은 경우 Lock 과 관련된 클래스를제공해 줍니다 -> RedissonClient
@Component
@RequiredArgsConstructor
public class RedissonLockStockFacade {
private final RedissonClient redissonClient;
private final StockService stockService;
public void decrease(final Long key, final Long quantity) {
//key 로 Lock 객체 가져옴
RLock lock = redissonClient.getLock(key.toString());
try {
//획득시도 시간, 락 점유 시간
boolean available = lock.tryLock(5, 1, TimeUnit.SECONDS);
if (!available) {
System.out.println("lock 획득 실패");
return;
}
stockService.decrease(key, quantity);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}
서비스레이어에서 바로 Redisson
을 사용하게 되면 테스트 코드에 실패합니다. 그 이유는 분산락 해제 시점과 트랜잭션 커밋 시점의 불일치 때문입니다. 서비스 단의 decrese 메서드에는 @Transactional 어노테이션이 붙어 있습니다. 때문에 스프링 AOP를 통해 decrease 메서드 바깥으로 트랜잭션을 처리하는 프록시가 동작하게 됩니다. 반면 락 획득과 해제는 decrease 메서드 내부에서 일어납니다. 때문에 스레드 1과 스레드 2가 경합한다면 스레드 1의 락이 해제되고 트랜잭션 커밋이 되는 사이에 스레드 2가 락을 획득하게 되는 상황이 발생합니다. 데이터베이스 상으로 락이 존재하지 않기 때문에 스레드 2는 데이터를 읽어오게 되고, 스레드 1의 변경 내용은 유실됩니다. 때문에 📢락 범위가 트랜잭션 범위보다 크도록 Facade를 만들어주도록 하겠습니다.
// StockServiceTest.java
@DisplayName("redis reddison lock 을 사용한 재고 감소 - 동시에 1000개 테스트 | 17.23s 소요")
@Test
void REDISSON_LOCK을_사용한_재고_감소() throws InterruptedException {
// given
// when
IntStream.range(0, threadCount).forEach(e -> executorService.submit(() -> {
try {
redissonLockStockFacade.decrease(productId, quantity);
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
} finally {
countDownLatch.countDown();
}
}
));
countDownLatch.await();
// then
final Long afterQuantity = stockRepository.getByProductId(productId).getQuantity();
System.out.println("### REDDISON LOCK 동시성 처리 이후 수량 ###" + afterQuantity);
assertThat(afterQuantity).isZero();
}
Redisson의 특징
- 락 획득 재시도를 기본으로 제공한다.
- pub-sub 방식으로 구현이 되어있기 때문에 lettuce 와 비교했을 때 redis에 부하가 덜 간다.
- 별도의 라이브러리를 사용해야한다.
- lock을 라이브러리 차원에서 제공해주기 때문에 사용법을 공부해야 한다.