[Project] Redis로 동시서 제어하기 - SETNX

이상혁·2024년 10월 26일

Project

목록 보기
5/12

들어가며

저번 포스트에서는 인기검색어를 구현을 하는데 있어서 Synchronized 키워드와 Optimistic Lock, Pessimitic Lock을 사용하고 어느 방법이 현재 프로젝트에 적합한 지를 알아 보았습니다. 그리고 Pessimitic Lock을 사용하는 것이 현재 프로젝트에서 적합하다는 판단까지 내렸습니다.
그런데 Pessimitic Lock이 속도가 걸린다는 단점이 있었습니다. 속도가 느리다는 것은 성능의 문제와 관련이 있습니다. 그래서 이 속도를 높히는 방법으로 In-Memory-Database인 Redis를 통해서 동시성을 제어를 해보고자 합니다. 이 포스트에서 Redis의 SETNX를 이용한 방법에 대해서 알아 보겠습니다.

Redis와 SETNX 간단하게 알아보자

Redis는 In-Memory-Database입니다. In-Memory-Datadase는 무엇일까요?

보통 데이터베이스는 디스크에 저장이 됩니다. 대표적으로 MySql, MariaDB, Postgresql 등이 있습니다. 이렇게 디스크에 저장이 되는 데이터 베이스는 비휘발성으로 영구적으로 저장이 가능합니다. 하지만 디스크에 저장이 되기 때문에 디스크에서 CPU까지 정보를 가지고 와야 합니다. 그러다 보면 시간이 오래 걸리는 단점이 있습니다.

In-Memory-Database는 메모리에 저장이 되는 데이터 베이스 입니다. 즉, 메모리에 데이터를 저장하고 읽는 것입니다. 메모리에 저장이 되기 때문에 디스크에 저장이 된 데이터보다 빠르게 가지고 올 수 있습니다. 하지만 메모리는 휘발성이기 때문에 서버가 다운이 되거나 컴퓨터가 종료가 되면 데이터가 유실이 되는 단점이 있습니다.

Redis는 In-Memory_Database 중 하나입니다. SETNX은 Redis에서 기본적으로 제공을 하는 원자성 명령어입니다. 주로 분산 락을 구현을 하거나 동시성을 제어하기 위해서 사용이 됩니다. Redis는 단일 스레드이기 때문에 원자성을 유지해 주는데 이러한 특성 때문에 SETNX가 원자적으로 실행이 가능합니다. SETNX명령어는 키가 존재하지 않을 때만 값을 설정을 하고 키가 있을 때는 작업을 진행을 하지 않습니다. 이 명령어가 바로 아래의 나와 있는 명령어입니다.

SETNX key value

이러한 방식을 이용을 해서 동시성을 제어를 하는 Lock을 만들 수 있습니다.

그렇다면 프로젝트에 어떻게 사용을 할 건데?

이제 redis와 SETNX을 이용해서 포로젝트의 인기 검색어에 적용을 해보겠습니다.

@Service
@RequiredArgsConstructor
public class PopularSearchService {

    private final StringRedisTemplate redisTemplate;

    private static final String LOCK_PREFIX = "lock:search:";
    private static final String SEARCH_RANKING_KEY = "popular:search:ranking";



    public void incrementSearchCount(String keyword) {
        String lockKey = LOCK_PREFIX + keyword;

        Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "LOCKED", 5, TimeUnit.SECONDS);

        if (lockAcquired) {
            try {
                updateSearchCount(keyword);
            } finally {
                releaseLock(lockKey);
            }
        } else {
            System.out.println("다른 프로세스에서 이미 락을 획득했습니다. 요청을 무시합니다.");
        }

    }

    private void releaseLock(String lockKey) {
        redisTemplate.delete(lockKey);
    }

    private void updateSearchCount(String keyword) {
        redisTemplate.opsForZSet().incrementScore(SEARCH_RANKING_KEY, keyword, 1);
    }

}

먼저, LOCK_PREFIX의 경우 Lock을 설정하기 위한 키의 접두사입니다. SEARCH_RANKING_KEY는 인기 검색어와 횟수가 저장이 되는 키의 접두사입니다.
incrementSearchCount메소드는 인기검색어의 count를 올려주는 메소드입니다. 이 메소드에서는 Redis의 setIfAbsent를 통해서 동시성을 제어를 하기 위한 Lock을 만들어 줍니다. lockKey를 만들어서 그 lock가 없으면 값을 "LOCKED"라는 값으로 설정을 하고 여기서 값을 얻는데 성공을 하면 True, 이미 값이 존재를 해서 실패를 하면 False를 반환을 합니다.
만약 True라면 updateSearchCount 메소드를 실행을 하는데 Sorted Set의 해당 검색어를 자동 연산을 시킵니다. 자동 연산이 되면 또 자동으로 Count 값에 의해서 자동 정렬이 됩니다. 그리고 finally 구문이 실행이 되면서 위에서 설정이 되었던 lockKey를 삭제를 해줍니다.

이제 결과를 볼까?

이제 이 로직을 했을 때 동시성 테스트를 해보겠습니다. 조건은 저번 포스트와 마찬가지로 8000으로 하였습니다.

결과를 보니 711이 나왔습니다. 제가 원하던 8000이라는 값은 나오지 않았습니다. 어떻게 된 것일까요?
이 방법은 Lock을 획득을 했을 때 작업을 처리하도록 했습니다. 즉, Lock을 획득을 하지 못 하면 작업을 할 수 없는 것입니다. 전에 사용을 했던 Pessimitic Lock의 경우 Lock을 얻지 못 하면 Lock을 얻을 때까지 대기를 합니다. 하지만 SETNX의 경우 Lock을 얻지 못 하면 대기를 하는 것이 아니라 바로 작업이 종료가 되게 됩니다. 이러한 이유로 8000의 동시성 테스트를 했을 때, 711만 카운트가 된 원인입니다.

이러한 방식을 보았을 때, 방법을 현재 프로젝트의 인기검색어 동시성 문제에 대해서 적합하지 않은 방법이라고 판단을 했습니다.

그래서 다음 포스트에서는 redis로 Spin Lock을 이용한 분산 락을 구현을 해서 동시성을 제어를 해보고 Pessimitic Lock과 비교을 어떤 방법이 더 적합한지 고민을 해보도록 하겠습니다.

profile
꾸준히!

0개의 댓글