이번 글에서는 최상용님의 "재고시스템으로 알아보는 동시성이슈 해결방법"을 정리해 보려고 합니다.
Lettuce
는 분산락을 구현할 때 사용하는 대표적인 라이브러리입니다.
SETNX
를 활용해 분산락을 구현합니다.
SETNX
=set if not exists
의 줄임말으로,
키가 존재하지 않을 경우 값을 지정하는 방식으로 동작합니다.
또한, 스레드가 락을 사용할 수 있는지 반복적으로 확인하면서 락 획득을 시도하는
spin lock
방식을 사용합니다.
이때 retry 로직은 개발자가 작성해줘야 합니다.
Redis-CLI에 접속해서 데이터가 없는 상태에서
setnx 1 lock
명령어를 실행하면
키가 1인 데이터가 없기 때문에 성공합니다.
다시 setnx 1 lock
명령어를 실행하면?
키가 1인 데이터가 존재하기 때문에 실패하게 됩니다.
@Component
public class RedisLockRepository {
private final RedisTemplate<String, String> redisTemplate;
public RedisLockRepository(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public Boolean lock(Long key) {
return redisTemplate
.opsForValue()
.setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3000));
}
public Boolean unlock(Long key) {
return redisTemplate.delete(generateKey(key));
}
private String generateKey(Long key) {
return key.toString();
}
}
@Component
public class LettuceLockStockFacade {
private final RedisLockRepository redisLockRepository;
private final StockService stockService;
public LettuceLockStockFacade(RedisLockRepository redisLockRepository, StockService stockService) {
this.redisLockRepository = redisLockRepository;
this.stockService = stockService;
}
public void decrease(Long id, Long quantity) throws InterruptedException {
while (!redisLockRepository.lock(id)) {
/**
* Lock 획득에 실패하면 Thread.sleep()을 사용해서 100ms 텀을 두고, 재시도
* 레디스의 부하를 줄임
*/
Thread.sleep(100);
}
try {
stockService.decrease(id, quantity);
} finally {
redisLockRepository.unlock(id);
}
}
}
Lettuce
은 Named Lock
과 비슷하지만,
세션 관리를 신경쓰지 않아도 된다는 점이 다릅니다.
Lettuce 장점
spring data redis
는 기본 구현체가 Lettuce
이기 때문에 별도의 라이브러리를 사용하지 않아도 됩니다.Lettuce 단점
spin lock
방식때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis에 부하가 갈 수 있습니다. 그렇기 때문에 스레드간의 락 획득 텀을 두어야 합니다.Redisson
은 PUB/SUB
기반의 Lock 구현을 제공합니다.
PUB-SUB
기반은 채널을 하나 만들고 락을 점유 중인 스레드가 락을 획득하려고 대기 중인 스레드에게 해체를 알려주면 안내를 받은 스레드가 락 획득 시도를 하는 방식입니다.
그렇기 때문에 retry 로직이 필요하지 않습니다.
메시지를 주고 받기 위해 터미널을 두 개 띄워줍니다.
ch1이라는 채널을 구독합니다.
다른 터미널에서 hello라는 메시지를 ch1 채널에 보내줍니다.
ch1을 구독하고 있는 터미널에서 메시지를 받을 수 있습니다.
// Redisson을 사용하기 위해 추가
implementation 'org.redisson:redisson-spring-boot-starter:3.25.0'
Redisson 장점
Lettuce
는 계속 락 획득을 시도하는 반면에 Redisson
은 락 해제가 되었을 때만 시도(PUB-SUB
기반의 구현)하기 때문에 레디스의 부하를 줄여줍니다.Redisson 단점
RedissonClient
를 사용해야 합니다.Lettuce
: 재시도가 필요하지 않은 경우에 사용
Redisson
: 재시도가 필요한 경우에 사용
MySQL
Redis
보다 성능이 좋지 않습니다.Redis
MySQL
보다 성능이 좋기 때문에 더 많은 요청을 처리할 수 있습니다.실무에서는
MySQL
을 사용Redis
를 도입