저번시간에 동시성 이슈가 무엇인지와, 이것을 해결하기 위한 하나의 프로세스, 여러대의 프로세스에서 발생하는 동시성 이슈 해결법에 대해서 알아보았다.
이제는 새로운 방식으로 해결하는 방법인 분산락에 대해서 알아보자.
분산락은 분산 시스템이나 마이크로서비스 아키텍처와 같이 여러 컴퓨터 또는 프로세스가 네트워크를 통해 서로 통신하며 작동하는 환경에서 사용되는 락이다.
락 상태를 비관적락 이나 낙관적락 처럼 데이터베이스의 내장 기능을 활용하는것이 아니고, 외부 시스템을 사용해서 락의 상태를 관리한다.
분산락을 구현할때, Redis를 사용할 수 있다.
왜 Redis를 사용할까?
DB를 이용하여 분산락을 구현해도 되지만, 락 정보는 영구적인 데이터가 아닌 휘발성 데이터에 더 가깝기 때문이다.
그리고 Redis가 Single Thread 기반이기 때문에 동시성을 제어하기에 좋다.
Setnx란?
SET if Not eXist의 줄임말로, 특정 Key 값이 존재하지 않을 경우에 set 하라는 명령어. 즉, value가 없을 때만 락을 획득하는 효과
코드로 구현해보자.
RedisLockRepository
@Component
public class RedisLockRepository {
private 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(3_000));
}
public Boolean unlock(Long key){
return redisTemplate.delete(generateKey(key));
}
private String generateKey(Long key) {
return key.toString();
}
}
퍼사드 패턴을 사용하여 RedisLockRepository와 StockService를 주입.
@Component
@RequiredArgsConstructor
public class LettuceLockStockFacade {
private final RedisLockRepository redisLockRepository;
private final StockService stockService;
public void decrease(final Long key, final Long quantity) throws InterruptedException {
// Lock 획득 시도
while (!redisLockRepository.lock(key)) {
//SpinLock 방식이 redis 에게 주는 부하를 줄여주기위한 sleep
Thread.sleep(100);
}
//lock 획득 성공시
try{
stockService.decrease(key,quantity);
}finally {
//락 해제
redisLockRepository.unlock(key);
}
}
}
여기서 Thread.sleep(100)은
Spin Lock 방식이 Lock을 얻을때까지 계속 시도하는데, 이때 Redis의 부하를 줄여주기 위해 사용.
Redisson은 pub/sub 기능을 사용하여 스핀 락이 레디스에 주는 트래픽을 줄였다.
락이 해제될 대마다 subscribe하는 클라이언트들에게 "락 획득을 시도하세요" 라는 알림을 주어서 각 쓰레드가 레디스에 요청을 보내 락의 획득가능여부를 체크하지 않아도 되도록 개선한 방식이다.
또한, Redisson은 RLock 이라는 락을 위한 인터페이스를 제공하여 손쉽게 락을 사용할 수 있다.
RedissonLockStockFacade
@Component
@RequiredArgsConstructor
public class RedissonLockStockFacade {
private final RedissonClient redissonClient;
private final StockService stockService;
@Transactional
public void decrease(Long id, Long quantity){
RLock lock = redissonClient.getLock(id.toString());
try{
boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);
if( !available){
System.out.println("Lock 획득 실패");
return;
}
stockService.decrease(id, quantity);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
동일하게 테스트를 진행하였다.