데이터 타입과 활용 사례
- 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으로 시간에 따라 감쇠.
처리 흐름
- 클라이언트 쓰기 요청.
- 메모리 사용량이
maxmemory초과인지 확인. - 정책에 따라 삭제할 키 결정.
- 키 삭제 및 메모리 회수.
- 메모리 여유가 생길 때까지 반복.
주요 문제 해결
- 일괄 만료로 인한 지연: 과도한 만료 시간을 분산시키기.
- 핫 데이터 삭제:
volatile-lru또는volatile-lfu사용, 핫 데이터는 만료 시간 미설정. - 메모리 증가, 정책 미작동:
maxmemory설정 확인,allkeys-*정책은 만료 시간이 없는 키는 대상이 아님.