일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- 프로그래머스
- docker
- spring event
- 검색어 추천
- 크롤링
- gRPC
- piplining
- 카카오
- 몽고 인덱스
- 이분탐색
- 추천 검색 기능
- 레디스 동시성
- 구현
- dau 3만명
- langgraph
- 베타적락
- 쿠키
- 셀러리
- ipo 매매자동화
- next-stock
- 디버깅
- 아키텍쳐 개선
- 누적합
- ai agent
- 완전탐색
- JPA
- BFS
- 백준
- 결제서비스
- AWS
- Today
- Total
코딩관계론
Redis 분산 시스템에서의 동시성 문제 해결 본문
최근 프로젝트에서 Redis를 사용하는 분산 시스템에서 동시성 이슈가 발생했습니다. 시스템이 scale-out 되면서 여러 스레드가 동시에 접근함에 따라 다양한 동시성 문제가 발생했습니다.
단일 스레드 환경에서의 동시성 문제
단일 스레드 환경에서는 동시성 문제가 발생하지 않습니다. 예를 들어, 다음 코드가 단일 스레드에서 동작할 때는 모든 연산이 순차적으로 실행되어 동시성 문제가 없습니다.
RScoredSortedSetAsync<String> scoredSortedSet = client.getScoredSortedSet(key);
RFuture<Double> scoreFuture = scoredSortedSet.firstScoreAsync();
RFuture<String> nameFuture = scoredSortedSet.pollFirstAsync();
멀티 스레드 환경에서 발생하는 문제
그러나 멀티 스레드 환경에서는 Redis에 명령이 비순차적으로 도착하여 예상치 못한 결과가 나타날 수 있습니다. 예를 들어, 두 스레드가 위의 명령어를 동시에 실행할 경우 결과가 (1, USER1), (1, USER2)
와 같이 잘못된 결과를 낼 수 있습니다.
해결 방안
동시성 문제를 해결하기 위한 주요 접근법은 다음과 같습니다.
1. 락(Lock)을 이용한 해결 방법
락을 사용하면 동시성 문제를 해결할 수 있으나, 모든 작업이 순차적으로 처리되므로 scale-out 환경의 성능 장점이 줄어듭니다. 또한 네트워크 왕복 시간이 늘어나면서 성능 저하가 발생할 수 있습니다.
RScoredSortedSetAsync<String> scoredSortedSet = client.getScoredSortedSet(key);
Redis.tryLock();
RFuture<Double> scoreFuture = scoredSortedSet.firstScoreAsync();
RFuture<String> nameFuture = scoredSortedSet.pollFirstAsync();
Redis.releaseLock();
2. MULTI/EXEC 및 Pipelining을 활용한 해결 방법
Redis의 MULTI/EXEC 트랜잭션과 Pipelining 기능을 활용하면 효율적으로 동시성 문제를 해결할 수 있습니다.
- MULTI/EXEC: 명령을 큐에 쌓았다가 EXEC를 호출할 때 일괄 처리합니다. Redis는 트랜잭션 중에도 다른 클라이언트 요청을 처리하며, EXEC 시점에만 일시적으로 요청 처리를 멈춥니다.
- Pipelining: 여러 명령을 클라이언트에서 수집한 후 서버에 한 번에 전송하여 네트워크 왕복 시간을 단축하고 성능을 향상시킵니다.
다음과 같은 방식으로 코드를 변경할 수 있습니다.
batch = client.createBatch(BatchOptions.defaults().executionMode(BatchOptions.ExecutionMode.REDIS_WRITE_ATOMIC));
RScoredSortedSetAsync<String> scoredSortedSet = batch.getScoredSortedSet(key);
RFuture<Double> scoreFuture = scoredSortedSet.firstScoreAsync();
RFuture<String> nameFuture = scoredSortedSet.pollFirstAsync();
batch.execute();
Redis-cli를 통해 모니터링하면 다음과 같은 결과를 확인할 수 있습니다.
[MULTI]
[ZRANGE] "EVENT:QUEUE:1" "0" "0" "WITHSCORES"
[EVAL] "local v = redis.call('zrange', KEYS[1], ARGV[1], ARGV[2]); if #v > 0 then redis.call('zremrangebyrank', KEYS[1], ARGV[1], ARGV[2]); return v; end return v;" "1" "EVENT:QUEUE:1" "0" "0"
[lua] "zrange" "EVENT:QUEUE:1" "0" "0"
[lua] "zremrangebyrank" "EVENT:QUEUE:1" "0" "0"
[EXEC]
결론
MULTI/EXEC 및 Pipelining 기능을 활용하면 Redis의 장점을 극대화하면서, 분산 시스템의 동시성 문제를 효율적으로 해결할 수 있습니다. 특히 대규모 트래픽 환경에서도 데이터 일관성을 보장하며 우수한 성능을 유지할 수 있습니다.
참고 자료
'개발' 카테고리의 다른 글
Bull을 BullMQ로 마이그레이션... (0) | 2025.05.20 |
---|---|
결제서비스 (0) | 2024.09.02 |
TPS 2에서 TPS 10,000까지의 험난한 과정 (0) | 2024.08.22 |
Redis 객체가 소멸될 때 DB에 영속화하자 - 꿀팁있음 (0) | 2024.08.19 |
검색어 추천 서비스 V4(Sharding) (0) | 2024.08.13 |