Redis 핵심 기능 및 아키텍처 분석

데이터 타입과 활용 사례

  • String: 캐싱, 카운터(인크리먼트/디크리먼트), 세션 저장. 최대 512MB 데이터 지원.
  • Hash: 객체 정보, 상품 상세 정보 저장. 필드-값 구조로 구성되며 HSET, HGET, HGETALL 명령어로 접근 가능.
  • List: 메시지 큐(예: LPUSH/RPOP), 최신 공지 리스트(LPUSH/LRANGE). 내부적으로 압축리스트 또는 양방향 링크드리스트 사용.
  • Set: 중복 제거, 친구 관계, 추첨 참가자 관리. 집합 연산(교집합, 합집합, 차집합)을 지원하며 해시테이블 기반으로 O(1) 시간 복잡도 보장.
  • Sorted Set: 랭킹 시스템, 범위 검색, 우선순위 작업 큐. 점수 기준 정렬, 스킵리스트와 해시테이블 조합으로 구현.
  • Bitmap: 사용자 행동 통계, 활성 사용자 분석, 권한 제어.
  • HyperLogLog: 웹사이트 유저 수(유니크 방문자), 앱 일일/월간 활성 사용자 수 계산.
  • Geo: 근처 사람/장소 추천, 거리 계산, 지역 범위 필터링. 위경도를 GeoHash로 변환하여 정렬된 집합으로 처리.

분산 잠금 구현 원리

분산 잠금은 여러 클라이언트가 공유 자원에 동시 접근하는 것을 방지하기 위한 메커니즘입니다. 주요 설계 목표는 다음과 같습니다:

  • 상호 배타성: 한 번에 하나의 클라이언트만 잠금을 소유.
  • 안전성: 잠금은 자신이 설정한 클라이언트만 해제할 수 있어야 함.
  • 사망 방지: 클라이언트가 다운되어도 만료 시간으로 인해 자동 해제.
  • 고가용성: Redis 장애 시에도 안정적인 동작 보장 필요.

기본적인 잠금 명령:

SET lock_key unique_token NX EX 30

잠금 해제는 Lua 스크립트로 원자성을 보장:

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

Redis 트랜잭션 모델

트랜잭션은 MULTI, EXEC, DISCARD로 제어됩니다.

  • MULTI: 트랜잭션 시작, 명령어는 대기열에 저장됨.
  • QUEUED: 각 명령어는 성공적으로 대기열에 추가됨.
  • EXEC: 모든 명령어를 순차 실행하고 결과 반환.
  • DISCARD: 트랜잭션 취소, 대기열 초기화.

원자성은 전체 실행 과정에서 중단되지 않지만, 오류 발생 시 일부 명령어는 실패하더라도 다른 명령어는 계속 실행됩니다.

옵티미스틱 락 (WATCH 명령)

WATCH는 트랜잭션 실행 전 키 변경 여부를 감시합니다. 만약 감시 중인 키가 수정되면 트랜잭션은 무효화됩니다.

WATCH balance
# 다른 클라이언트가 값을 변경하면
EXEC → nil (실패)

Redisson 분산 잠금의 강점

Redisson는 원시 명령보다 더 견고한 분산 잠금 솔루션입니다.

  • 재진입 가능성: 같은 스레드가 동일한 잠금을 여러 번 획득 가능. 해시 구조에 재진입 횟수 저장.
  • 자동 갱신 (Watchdog): 잠금 만료 시간의 1/3 주기로 자동 갱신. 기본 30초, 10초 간격.
  • 다양한 잠금 유형: 공정 잠금, 멀티락, 레드락 등 지원.
  • 성능 최적화: 발행/구독 기반 이벤트 처리로 루프 확인 부담 감소.
  • 고가용성: Redis 클러스터 및 주/복제 환경 모두 호환.
  • 간편한 사용성: Java 락 인터페이스와 유사한 메서드 제공.

Watchdog와 시간 휠 알고리즘

Watchdog는 잠금 유지 시간을 자동 갱신하는 백그라운드 스레드입니다. 잠금을 보유 중인 경우 주기적으로 PEXPIRE 명령으로 만료 시간을 늘립니다.

시간 휠 (Time Wheel)은 많은 지연 작업을 효율적으로 관리하는 알고리즘입니다. 반복 배열 형태로, 각 버킷은 특정 시간 간격을 의미하며, 작업은 해당 버킷에 배치됩니다. 외부 타이머가 매 단계마다 진행하면서 현재 버킷의 작업을 실행합니다. 고급 시나리오에서는 다층 구조로 확장됩니다.

내부 데이터 구조 및 확장 메커니즘

  • String: SDS(Simple Dynamic String) 구조로 문자열을 관리. 길이, 남은 공간, 실제 데이터 포함.
  • List: Redis 3.2 이후에는 QuickList 사용. 각 노드는 Ziplist로 구성되어 메모리 효율성과 성능 균형.
  • Hash: 요소 수 및 크기가 작으면 Ziplist, 그렇지 않으면 Hashtab. 리해싱 충돌은 체이닝 방식으로 해결.
  • Set: 정수만 포함되고 수가 적으면 Intset, 아니면 Hashtab. 정렬된 배열 기반 이진 탐색 지원.
  • Sorted Set: 작은 요소는 Ziplist, 큰 경우 스킵리스트 + 해시테이블 조합. 점수 기준 정렬, 빠른 조회 및 범위 검색 가능.

확장 (Rehashing)

  • 새 해시 테이블 생성: 기존 크기의 2배 이상인 최소 2의 제곱수.
  • 점진적 마이그레이션: 각 요청 시 하나의 버킷씩 이전 테이블에서 새 테이블로 옮김.
  • 정기 작업: 1밀리초당 최대 100개의 버킷 이동.
  • 완료 후, 기존 테이블 삭제. 조회 시 두 테이블 모두 검색.

만료 및 메모리 정책

만료 전략

  • 관찰적 삭제: 키 접근 시 만료 여부 확인. 메모리 누수 가능성 있음.
  • 주기적 삭제: serverCron이 1초에 10회 실행. 무작위 20개 키 검사, 만료 시 삭제.
  • 메모리 부족 시 삭제: 할당된 메모리 초과 시 정책 적용.

메모리 제거 전략 비교

정책규칙사용 사례
noeviction쓰기 요청 거부 (기본)데이터 손실 금지
allkeys-lru모든 키 중 가장 오래 사용하지 않은 것 삭제핫 데이터 뚜렷한 경우
volatile-lru만료 시간 설정된 키 중 가장 오래 사용하지 않은 것 삭제임시/영구 데이터 구분 필요
allkeys-lfu모든 키 중 가장 덜 사용된 것 삭제 (Redis 4.0+)고빈도 데이터 유지 필요
volatile-lfu만료된 키 중 가장 덜 사용된 것 삭제 (Redis 4.0+)임시 데이터의 빈도 구분
allkeys-random무작위 키 삭제접근 패턴 없음
volatile-random만료된 키 중 무작위 삭제임시 데이터 무작위 삭제
volatile-ttl남은 수명이 가장 짧은 키 삭제곧 만료될 데이터 우선 제거

LRU vs LFU 구현

  • LRU: 24비트 타임스탬프로 최근 사용 시간 저장. 5개 키 중 가장 오래된 것 선택 (최적화된 근사).
  • LFU: 8비트 카운터로 접근 빈도 기록. lfu-decay-time으로 시간에 따라 감쇠.

처리 흐름

  1. 클라이언트 쓰기 요청.
  2. 메모리 사용량이 maxmemory 초과인지 확인.
  3. 정책에 따라 삭제할 키 결정.
  4. 키 삭제 및 메모리 회수.
  5. 메모리 여유가 생길 때까지 반복.

주요 문제 해결

  • 일괄 만료로 인한 지연: 과도한 만료 시간을 분산시키기.
  • 핫 데이터 삭제: volatile-lru 또는 volatile-lfu 사용, 핫 데이터는 만료 시간 미설정.
  • 메모리 증가, 정책 미작동: maxmemory 설정 확인, allkeys-* 정책은 만료 시간이 없는 키는 대상이 아님.

태그: Redis 데이터 구조 분산 잠금 트랜잭션 메모리 관리

6월 11일 17:34에 게시됨