백엔드 관련 강의 공부/동시성 이슈 - 재고시스템

redis를 활용한 동시성 문제 해결 - 1. Lettuce

chanhee01 2024. 2. 4. 14:11

Lettuce 라이브러리는 setnx 명령어를 활용하여 분산락을 구현하는 방식이다. setnx 명령어는 key와 value를 set할 때 기존의 값이 없을 때에만 set을 하는 명령어이다. setnx를 이용하는 방식은 spin lock 방식이므로 retry 로직을 개발자가 직접 작성해야 한다.

 

setnx에 대해 설명하기 위해 터미널에서 도커로 docker exec -it <container_id> redis-cli를 통해 레디스에 접속해서 명령어를 입력했다. setnx <key> <value> 형태로 입력을 했는데, 처음의 setnx 1 lock은 key가 1인 값이 없기 때문에 1을 반환해준다. 그 이후에는 key가 1인 값이 존재하기 때문에 0을 반환해주고, del 1로 삭제한 다음에 setnx 1 lock을 하면 다시 1을 반환해준다.

 

위의 방식을 코드로 구현해서 spin lock을 구현한다.

 

 

 

 

RedisLockRepository.class

@Repository
@RequiredArgsConstructor
public class RedisLockRepository {

    private final RedisTemplate<String, String> 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();
    }
}

우선 redis를 사용해야되기 때문에 redis repository를 만들어준다. 로직 실행 전에 key와 setnx 명령어를 통해 lock을 하고, 로직이 끝나면 unlock 메서드를 통해서 lock을 해제한다.

 

 

@Component
@RequiredArgsConstructor
public class LettuceLockStockFacade {

    private final RedisLockRepository redisLockRepository;
    private final StockService stockService;

    public void decrease(Long id, Long quantity) throws InterruptedException {
        while (!redisLockRepository.lock(id)) {
            Thread.sleep(100);
        }

        try {
            stockService.decrease(id, quantity);
        } finally {
            redisLockRepository.unlock(id);
        }
    }
}

만약에 lock이 사용중이어서 setnx 명령어를 통해 lock을 하지 못한다면 Thread.sleep(100);으로 while문을 반복하고, lock을 획득한다면 decrease를, 로직이 정상적으로 수행되면 마지막에 unlock을 해주는 로직이다.