Split View: Redis 내부와 분산 캐시 — 싱글 스레드, 자료구조, Cluster, Sentinel, RDB/AOF, Redlock, Valkey, Dragonfly 완전 정복 (2025)
Redis 내부와 분산 캐시 — 싱글 스레드, 자료구조, Cluster, Sentinel, RDB/AOF, Redlock, Valkey, Dragonfly 완전 정복 (2025)
"Redis is what happens when a C programmer falls in love with data structures." — Salvatore Sanfilippo (antirez)
Redis만큼 **"그냥 쓴다"**와 "제대로 이해하고 쓴다" 사이의 간극이 큰 시스템도 드물다. 대부분의 개발자는 GET/SET 두 명령만 쓰고, 그것도 문자열 캐시 용도로만 쓴다. 그러나 Redis는 사실 인메모리 데이터 구조 서버(In-Memory Data Structure Server) — 캐시는 부산물일 뿐이다.
2009년 Salvatore Sanfilippo가 자신의 웹 분석 도구 LLOOGG를 위해 만들었다. MySQL로는 실시간 랭킹을 유지할 수 없어서 직접 만든 "Remote Dictionary Server"가 Redis의 시작이다. 2010년 VMware가 고용, 2013년 Pivotal, 2015년 Redis Labs(현 Redis Inc)로 이어졌다. 그리고 2024년 3월, Redis는 갑자기 오픈소스 라이선스를 버렸다. Linux Foundation이 Valkey를 포크하면서 클라우드 전쟁의 새 국면이 열렸다.
이 글은 Redis를 "정말로" 이해하려는 사람을 위한 지도다.
1. 왜 Redis는 빠른가 — 싱글 스레드 역설
흔한 오해
"싱글 스레드인데 빠르다니, 코어를 놀리는 거 아닌가?"
아니다. Redis 6.0부터는 네트워크 I/O는 멀티스레드로 처리하지만, 명령 실행 자체는 여전히 싱글 스레드다. 그리고 이것이 빠른 이유다.
싱글 스레드의 합리성
- 메모리 속도 = CPU 속도에 가까움 — 병목이 네트워크와 OS 호출에 집중된다
- 락 없음 — 공유 자료구조에 락/뮤텍스 오버헤드 제로
- 컨텍스트 스위치 없음 — CPU 캐시 locality 최대
- 원자성이 자연스러움 — 모든 명령이 atomic
- 디버그/추론 쉬움 — 버그 재현이 단순하다
antirez의 말: "이미 2009년에 나는 락 기반 멀티스레딩이 현실적으로 어렵다는 것을 알고 있었다. 락 없이 빠른 것을 만들자."
그런데 어떻게 100만 QPS?
실제로 Redis는 단일 인스턴스에서 초당 100만 명령을 넘게 처리한다. 비결은:
- I/O 멀티플렉싱 —
epoll(Linux)/kqueue(BSD) 이벤트 루프로 한 스레드에서 수천 개의 소켓을 동시에 감시 - RESP 프로토콜 — 단순한 텍스트 기반, 파싱 비용 최소
- 파이프라이닝 — 한 번에 여러 명령을 보내고 응답을 묶어서 받음
- 제로카피 아님 — 하지만 작은 단위라 문제 없음
- 대부분 또는 자료구조
Event Loop 한 바퀴
1. epoll_wait() — 읽기/쓰기 준비된 소켓 찾기
2. 요청 파싱 (RESP)
3. 명령 실행 (싱글 스레드, 메모리 조작만)
4. 응답 버퍼에 쓰기
5. 다음 이벤트로 이동
한 명령이 오래 걸리면? 전체가 멈춘다. 그래서 KEYS *나 FLUSHALL은 프로덕션에서 절대 금지. 대신 SCAN, UNLINK를 쓴다.
2. 자료구조 — Redis의 진짜 힘
Redis의 OBJECT ENCODING mykey로 내부 인코딩을 확인해보면 놀랄 것이다. 같은 "String"이라도 숫자면 int, 짧으면 embstr, 길면 raw로 다르게 저장된다.
9가지 핵심 자료구조
| 자료구조 | 사용처 | 내부 구현 | 특이점 |
|---|---|---|---|
| String | 문자열/숫자/바이너리 | SDS (Simple Dynamic String) | 512MB까지 |
| List | 큐/스택 | QuickList (ziplist+linkedlist) | 양방향 O(1) push/pop |
| Hash | 객체 필드 | listpack/hashtable | ziplist로 작은 해시 최적화 |
| Set | 고유값 집합 | listpack/intset/hashtable | 숫자만 있으면 intset |
| Sorted Set | 랭킹/우선순위 큐 | Skip List + hash | 역사적으로 흥미로운 선택 |
| Stream | 이벤트 로그 | Radix Tree | Kafka-like consumer group |
| HyperLogLog | 카디널리티 추정 | 12KB 고정, 표준편차 0.81% | 확률적 자료구조 |
| Bitmap | 비트 배열 | String 기반 | 10억 유저 DAU를 128MB에 |
| Geospatial | 위치 질의 | Sorted Set + Geohash | GEOADD/GEORADIUS |
SDS — 왜 C 문자열이 아닌가
struct sdshdr {
int len; // O(1) strlen
int free; // 여분 공간 (재할당 감소)
char buf[]; // 실제 데이터 + '\0'
};
strlenO(1) (C 문자열은 O(N))- Binary safe (중간에
\0포함 가능) - 재할당 횟수 감소 (2배 버퍼링)
Sorted Set의 Skip List — 왜 Red-Black Tree가 아닌가
antirez가 Skip List를 선택한 이유 (직접 블로그에 썼다):
- 구현이 단순 — B-Tree/Red-Black Tree 대비 절반 이하
- Range query 최적화 — 정렬된 링크드리스트 구조
- 메모리 locality가 적당히 좋음
- 디버깅 쉬움 — 트리 회전 없음
"내가 Skip List를 고른 것은 그것이 최적이어서가 아니라, 구현이 단순했기 때문이다." — antirez
HyperLogLog — 12KB로 수십억 카디널리티 추정
- "오늘 방문한 UV가 몇 명?" → Set으로 하면 메모리 폭발
- HLL은 확률적 자료구조로 표준편차 0.81%로 추정
- 고정 12KB — 1억 명도 12KB, 100억 명도 12KB
PFADD visitors user:1 user:2 user:3
PFCOUNT visitors # 근사치 반환
PFMERGE today yesterday # 집합 합집합 근사
Bitmap — SETBIT의 위력
SETBIT user:active:20260415 12345 1 # user 12345 오늘 접속
BITCOUNT user:active:20260415 # 오늘 DAU
BITOP AND weekly user:active:* # 주간 활성
10억 유저 → 10억 비트 → 128MB. RDB보다 훨씬 빠르게 "지난 30일 연속 접속 사용자" 같은 질의가 가능.
Stream — 2018년 추가, Kafka-lite
Redis 5.0부터. Consumer Group + offset 관리로 Kafka-lite 용도. 단, 영속성은 Redis persistence에 의존하므로 Kafka 대체라기보다 경량 메시지 버스.
3. 영속성 — RDB vs AOF, 그 미묘한 트레이드오프
Redis는 인메모리지만 데이터 손실을 막기 위해 두 가지 영속성을 제공한다.
RDB (Redis Database) — 스냅샷
- 주기적으로 전체 데이터셋을 바이너리 파일로 덤프
BGSAVE— fork() 후 자식 프로세스가 덤프, 부모는 계속 서비스- Copy-on-Write 덕에 부모 메모리 2배가 되지 않음(대개)
- 단점: fork 시점 이후 데이터는 손실 가능
- 장점: 재시작이 빠름, 파일 작음
AOF (Append-Only File) — 로그
- 모든 쓰기 명령을 파일 끝에 append
fsync정책:always— 매 명령마다 fsync (느림, 손실 없음)everysec— 1초마다 fsync (기본값, 최대 1초 손실)no— OS 맡김 (빠름, 손실 큼)
- AOF Rewrite — 주기적으로 압축 (현재 상태를 재구성하는 최소 명령만)
- 단점: 재시작이 느림 (모든 명령 재생), 파일 큼
- 장점: 손실 거의 없음
혼용 전략 — Redis 4.0+
aof-use-rdb-preamble yes — AOF 파일 앞에 RDB를, 뒤에 증분 AOF를 붙인다. 빠른 재시작 + 적은 손실을 모두 얻는다. 현재 사실상 표준.
의사결정 매트릭스
| 시나리오 | RDB | AOF | 혼용 |
|---|---|---|---|
| 캐시 용도 | O (영속성 불필요) | X | X |
| 세션 저장 | X | O (everysec) | O |
| 재시작 시간 중요 | O | X | O (RDB preamble) |
| 주 저장소 | X | O (always) | O (best) |
| 디스크 I/O 예민 | O | X | 조심 |
그런데 진짜 영속 저장소로 쓸 수 있나?
권장하지 않는다. antirez도 여러 번 경고했다. Redis는 "빠른 캐시 + 최소 데이터 손실"이 목표이지 "완벽한 내구성"이 목표가 아니다. 중요한 데이터는 Postgres/MySQL에 쓰고 Redis는 캐시로 쓰자.
4. Redis Cluster — Hash Slot의 예술
왜 Cluster?
- 단일 Redis는 메모리/처리량 한계 (보통 한 인스턴스 수십 GB)
- 멀티 샤드가 필요 → Redis Cluster (3.0, 2015년)
Hash Slot — 16384개
- 키를 CRC16으로 해싱 후 16384(2^14)으로 모듈로
- 각 슬롯을 마스터 노드에 할당
- 예: 3 마스터라면 각 ~5461 슬롯
왜 16384인가
antirez가 직접 답한 유명한 GitHub 이슈:
- 각 노드가 보유한 슬롯을 비트맵으로 전송하는데, 16384비트 = 2KB
- 65536이면 8KB — 가십 프로토콜에서 너무 크다
- 1000개 미만 클러스터에서는 16384가 분배 품질에 충분
MOVED & ASK — 리다이렉션
클라이언트가 잘못된 노드에 요청하면:
MOVED <slot> <host>:<port>— "이 슬롯은 영구적으로 저기 있다"ASK <slot> <host>:<port>— "지금 마이그레이션 중이야, 한 번만 저기 물어봐"
스마트 클라이언트(Lettuce, redis-py-cluster)는 자동으로 슬롯 맵을 캐시하고 MOVED가 오면 갱신한다.
Hash Tag — 같은 슬롯에 보장
SET {user:1}:profile "..."
SET {user:1}:sessions "..."
# 둘 다 CRC16("user:1") 기준으로 해싱되어 같은 슬롯
# 따라서 MULTI/EXEC, SUNION 같은 멀티 키 명령 가능
{} 안의 부분만 해싱에 쓰인다. 트랜잭션이나 Lua 스크립트에서 여러 키를 다룰 때 필수.
Gossip & Failure Detection
- 노드들끼리
PING/PONG으로 상태 교환 cluster-node-timeout넘으면PFAIL(주관적 실패)- 다수의 노드가 PFAIL 보고하면
FAIL(객관적 실패) - Replica가 자동 승격
한계
- 복잡 질의(
SINTER) 같은 멀티키 명령은 같은 슬롯 강제 - 크로스 슬롯 트랜잭션 불가
- 백업이 복잡 (노드별로 따로)
5. Sentinel — 고가용성의 또 다른 길
Cluster가 샤딩+HA를 함께 제공한다면, Sentinel은 단순 HA만 제공한다.
구조
- 마스터 1 + 리플리카 N + Sentinel M (보통 3개 이상, 홀수)
- Sentinel들이 마스터 상태를 모니터링
- 장애 시 Raft-유사 합의로 새 리더 선출
- 클라이언트는 Sentinel에 먼저 물어보고 현재 마스터를 알아냄
Cluster vs Sentinel
| 측면 | Sentinel | Cluster |
|---|---|---|
| 샤딩 | X | O |
| HA | O | O |
| 설정 복잡도 | 낮음 | 중간 |
| 앱 클라이언트 지원 | 넓음 | Smart client 필요 |
| 운영 규모 | 소~중 | 중~대 |
| 멀티키 명령 | 완전 지원 | Hash Tag 필요 |
선택 기준: 데이터가 한 노드 메모리에 들어가면 Sentinel, 안 들어가면 Cluster.
6. 캐시 패턴 — 뜨거운 감자
Cache-Aside (Lazy Loading)
def get_user(id):
user = redis.get(f"user:{id}")
if user is None:
user = db.query(id)
redis.set(f"user:{id}", user, ex=3600)
return user
- 가장 흔한 패턴
- 장점: 구현 쉬움, 캐시 미스만 DB 부담
- 단점: 처음 요청이 느림, Stale 데이터 가능
Write-Through
def update_user(id, data):
db.update(id, data)
redis.set(f"user:{id}", data, ex=3600) # 동시에 갱신
- 쓰기마다 DB + 캐시 모두 갱신
- 캐시 일관성 좋음
- 단점: 쓰기 지연 증가, 잘 안 읽히는 데이터도 캐시
Write-Behind (Write-Back)
def update_user(id, data):
redis.set(f"user:{id}", data)
queue.push({"id": id, "data": data})
# 워커가 배치로 DB 쓰기
- 쓰기 지연 최소
- 단점: 데이터 손실 위험, 구현 복잡
Refresh-Ahead
- TTL 임박한 캐시를 비동기로 미리 갱신
- Thundering Herd(아래) 방지
실전 조합
대부분의 프로덕션은 Cache-Aside + TTL + (조건부) Write-Through. 중요한 건 TTL 전략:
- 짧은 TTL (1-5분) — 최신성 중요
- 긴 TTL (1시간+) — 변경 적은 데이터
- Jitter —
ex=3600 + random(-300, 300)— 동시 만료 방지
7. Thundering Herd & Cache Stampede
가장 흔하면서 가장 은밀한 Redis 장애 원인.
시나리오
- 인기 키가 만료됨
- 동시에 수천 요청이 캐시 미스
- 모두 DB로 몰림 → DB 과부하 → 전체 장애
해결책 1 — Mutex Lock
def get_with_lock(key):
value = redis.get(key)
if value is not None:
return value
lock = redis.set(f"lock:{key}", "1", nx=True, ex=10)
if not lock:
time.sleep(0.05)
return redis.get(key) # 다른 프로세스가 채웠기를 기대
try:
value = db.query(...)
redis.set(key, value, ex=3600)
return value
finally:
redis.delete(f"lock:{key}")
해결책 2 — Probabilistic Early Expiration (XFetch)
만료 시간 전에 확률적으로 갱신. 논문 "Optimal Probabilistic Cache Stampede Prevention".
def fetch(key, beta=1.0):
value, ttl, delta = redis.get_with_meta(key)
now = time.time()
if value is None or now - delta * beta * math.log(random.random()) >= ttl:
value = db.query(...)
redis.set(key, value, ex=3600)
return value
해결책 3 — 이중 TTL (Soft & Hard)
- Soft TTL 지나면 백그라운드 갱신, 클라이언트는 낡은 값 반환
- Hard TTL 지나면 그때 동기 갱신
8. 분산 락 — Redlock 논쟁
Redis를 분산 락으로 쓰는 것은 매력적이다. SET key value NX PX 10000으로 간단히 구현.
단순 락 — 한 인스턴스
SET lock:resource unique_id NX PX 30000
# 성공하면 락 획득
# 해제 (Lua로 atomic하게 — unique_id 확인)
EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:resource unique_id
Redlock — 5개 인스턴스 기반 분산 락
antirez가 제안한 알고리즘:
- 5개 독립 Redis 인스턴스에 동시에 락 시도
- 과반(3개) 성공 + 총 시간이 TTL보다 작으면 락 획득
- 실패 시 모두 해제
Martin Kleppmann의 비판
DDIA 저자가 유명한 블로그로 반박:
- Fencing token이 없음 — 프로세스가 GC 정지되면 만료된 락으로 작업 계속
- 시계 비동기 가정 약함 — NTP 튐, VM 정지 등으로 TTL 보장 불가
- 정확성(correctness)이 필요하면 Redlock 쓰지 말라 — ZooKeeper, etcd 써라
antirez의 반박
- Redlock은 성능 목적 락 — 간헐적 중복 실행이 용납되는 경우에만
- 정확성이 치명적이라면 DB 트랜잭션을 써라
- Fencing token은 구현 가능 (단조 증가 카운터)
현실의 결론
| 목적 | 권장 도구 |
|---|---|
| 중복 방지(성능) | Redis SET NX PX |
| 리더 선출(정확성) | ZooKeeper/etcd |
| 트랜잭션 | DB 자체 |
| 결제/돈 관련 | 절대 Redis 락 금지 |
9. 2024년 라이선스 사태와 Valkey 포크
2024년 3월 20일, Redis Inc는 충격적 발표:
- Redis 7.4부터 BSD → SSPL/RSALv2 이중 라이선스
- 클라우드 사업자(AWS ElastiCache 등)에게 타격을 주려는 의도
- 오픈소스 커뮤니티는 분노
Valkey — 48시간 만의 반격
- 3월 28일, Linux Foundation이 Valkey 프로젝트 시작
- AWS, Google Cloud, Oracle, Ericsson이 창립 스폰서
- Redis 7.2.4에서 포크, BSD 3-Clause 유지
- 주요 메인테이너 대부분 Valkey로 이동
antirez의 복귀
2024년 11월, antirez가 Redis Inc로 복귀. 2025년에는 벡터 검색(RedisVL), AI 통합에 집중.
2025년 현재 상황
| 제품 | 라이선스 | 주도 | 기여자 |
|---|---|---|---|
| Redis | SSPL/RSALv2 | Redis Inc | antirez 복귀 |
| Valkey | BSD 3-Clause | Linux Foundation | AWS/Google/Oracle |
| KeyDB | BSD 3-Clause | Snap | 멀티스레드 포크 |
| Dragonfly | BSL/Apache 2.0 | Dragonfly Labs | 처음부터 재작성 |
선택 가이드:
- 퍼블릭 클라우드 managed service 쓸 것 → 상관 없음 (ElastiCache는 Valkey 전환 중)
- 자체 운영 + 오픈소스 고집 → Valkey
- 멀티스레드 극한 성능 → Dragonfly
- Redis 7.4+ 신기능 필요 → Redis
10. Dragonfly — "Redis를 25배 빠르게"
2022년 등장, C++20으로 처음부터 재작성. 비결:
- 멀티스레드 Shared-nothing — 각 스레드가 자기 샤드 담당, 락 불필요
- io_uring — Linux의 최신 비동기 I/O (기존 epoll보다 빠름)
- Dash — 학술논문 기반 해시테이블 — 캐시 친화적
- RDB 저장 속도 30배 — 메모리 스냅샷 알고리즘 혁신
성능 (2025 벤치마크)
- 단일 AWS c7g.16xlarge: 650만 QPS (Redis는 ~200K QPS)
- 메모리 효율도 30% 개선
한계
- 스크립팅 지원 제한 (Lua)
- Cluster 프로토콜 100% 호환 아직
- 일부 edge case 명령 미구현
- 운영 도구 생태계 작음
KeyDB
- Snap(Snapchat)이 만든 멀티스레드 포크
- Redis와 거의 100% 호환
- 2024년 이후 개발 정체 — Dragonfly로 흐름이 옮겨감
11. 메모리 관리 — OOM을 막는 8가지
Redis에서 메모리 부족은 즉시 장애. 관리 원칙:
maxmemory + Eviction Policy
maxmemory 4gb
maxmemory-policy allkeys-lru
정책:
noeviction— 새 쓰기 거부 (기본값, 위험)allkeys-lru— 모든 키 중 LRU 제거volatile-lru— TTL 있는 키 중 LRUallkeys-lfu— LFU (4.0+, 추천)volatile-ttl— TTL 임박 우선allkeys-random/volatile-random— 랜덤
캐시 용도면 allkeys-lfu 권장. LRU는 스캔 공격(한 번 읽고 다시 안 쓰는 키)에 취약.
메모리 프로파일링
MEMORY USAGE user:1
MEMORY STATS
MEMORY DOCTOR # 진단
Big Key 문제
단일 키가 1MB 넘으면 위험:
KEYS순회 비용- 네트워크 전송 지연
- Cluster 재샤딩 시 전체 이동
- 해결: 해시/리스트로 분할
Hot Key 문제
단일 키에 요청 집중:
- CPU 한 코어가 풀로 돌아감
- 해결: 클라이언트측 캐시, 샤드 분산, Replica로 읽기 분산
TTL 전략
- 반드시 TTL 설정 — 무한 키는 메모리 누수
- Jitter 추가 — 동시 만료 방지
- Hot key는 TTL 길게, Cold는 짧게
12. Lua 스크립팅 & Functions
Redis는 Lua 5.1 인터프리터를 내장. 여러 명령을 atomic하게 실행.
-- 재고 차감 (race condition 없이)
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
else
return 0
end
Functions (Redis 7.0+)
Lua 스크립트를 서버에 라이브러리로 저장 (함수 단위 관리).
주의
- Lua 실행 중 다른 모든 명령 블록 — 오래 걸리는 스크립트 금지
EVAL자주 쓰면EVALSHA+ SCRIPT LOAD로 캐시
13. Client-Side Caching (Tracking) — Redis 6.0
Redis가 서버 측 변경을 클라이언트에 푸시 알림. 클라이언트는 로컬 캐시를 유지하다가 무효화.
CLIENT TRACKING ON REDIRECT 1234 BCAST PREFIX user:
- Roundtrip 제거 → 지연 극도로 짧아짐
- 대부분의 현대 드라이버(Lettuce, redis-py)가 지원
- 단, 클라이언트 메모리 관리 필요
14. 모니터링 지표 — 꼭 봐야 할 10개
| 지표 | 설명 | 경고선 |
|---|---|---|
used_memory / maxmemory | 메모리 사용률 | 80% |
evicted_keys | 축출된 키 수 | 증가 추세 |
connected_clients | 동시 연결 | 1만+ 주의 |
instantaneous_ops_per_sec | QPS | 베이스라인 대비 |
latency_percentiles_usec_* | p99 지연 | 1ms 초과 |
rejected_connections | 연결 거부 | 0 유지 |
keyspace_hits / keyspace_misses | 캐시 히트율 | 90% 이상 |
aof_current_size | AOF 크기 | 디스크 여유 |
rdb_last_save_time | 마지막 RDB | 오래되면 경고 |
master_link_status (Replica) | 복제 연결 | up |
느린 명령 추적
CONFIG SET slowlog-log-slower-than 10000 # 10ms 이상
SLOWLOG GET 10
MONITOR 금지
MONITOR는 모든 명령을 스트리밍 → 성능 50%+ 저하. 운영에서는 절대 금지. 대신 SLOWLOG, LATENCY 명령.
15. 캐시 전략 안티패턴 TOP 10
- TTL 없는 키 대량 삽입 — 메모리 누수
- KEYS * 또는 FLUSHALL — 싱글 스레드 정지
- 거대한 Hash/List 누적 — 한 명령이 초 단위
- 캐시에만 데이터 저장 — Redis 손실 시 복구 불가
- 모든 키 같은 TTL — 동시 만료 → Stampede
- 분산 락으로 돈 관리 — Redlock 논쟁 참조
- Pub/Sub으로 영속 큐 — 메시지 사라짐, Stream 써라
- TTL 짧은 Write-Through — 쓰기마다 DB+Redis, 의미 없음
- Replica로 쓰기 — 복제는 비동기, 데이터 손실
- 클러스터에서 크로스 슬롯 트랜잭션 시도 — 조용히 실패
16. Redis를 현명하게 쓰는 체크리스트
- 용도가 캐시인가, 저장소인가 명확히 구분
-
maxmemory와 eviction policy 명시적 설정 - 모든 키에 TTL — Jitter 포함
- AOF + everysec로 1초 이내 손실 허용, 혼용 모드 고려
- MONITOR / KEYS * / FLUSHALL 금지 정책
- Big Key / Hot Key 경보 설정 (1MB/10k QPS)
- 캐시 히트율 90%+ 목표, 미스면 왜인지 분석
- Thundering Herd 대비 — Mutex 또는 XFetch
- 분산 락은 성능 목적만 — 정확성이 필요하면 다른 도구
- Cluster vs Sentinel 결정 기준 명확 (데이터 크기)
- Client-Side Caching 검토 — 읽기 집중 워크로드
- Replica 승격 시나리오 훈련 (Failover 테스트)
마치며 — 싱글 스레드의 우아함
Redis의 성공은 역설이다. 병렬이 왕인 시대에, 싱글 스레드로 초당 100만 QPS를 내는 시스템. 그 뒤에는 antirez의 철학이 있다.
"복잡한 것을 단순하게 만드는 게 아니라, 처음부터 단순하게 설계하는 것이 진짜 엔지니어링이다." — antirez
많은 "레거시" 기술이 그렇듯, Redis는 보기보다 훨씬 깊다. 자료구조, I/O 모델, 영속성, 분산, 트레이드오프 — 모든 결정에 이유가 있다. 2024년 라이선스 논쟁은 오픈소스와 상업화의 오래된 긴장이 Redis에서 터진 사건이고, Valkey/Dragonfly의 부상은 "Redis 그 자체보다 그 인터페이스가 더 중요해진" 시대를 보여준다.
다음 글 예고 — PostgreSQL 내부와 쿼리 최적화
Redis가 "데이터 구조의 우아함"이라면, PostgreSQL은 "관계형 DB의 완결판"이다. 다음 글에서는:
- MVCC — 왜 PostgreSQL은 Oracle과 다르게 구현했나 (tuple versioning)
- VACUUM의 비밀 — dead tuple, wraparound, autovacuum tuning
- WAL과 Streaming Replication — Physical vs Logical 복제
- Query Planner 내부 — 왜 같은 쿼리가 느리거나 빠른가 (EXPLAIN ANALYZE 읽기)
- Index의 세계 — B-Tree, Hash, GiST, GIN, BRIN, HNSW (pgvector)
- Partitioning & Sharding — Declarative partitioning, Citus
- pgBouncer & Connection Pooling — 왜 PostgreSQL은 연결이 비싼가
- JSONB vs Document DB — "Postgres로 다 된다"는 신화의 진실
- pgvector / pg_duckdb — AI와 분석 워크로드를 품은 PostgreSQL
- PostgreSQL 18(2025)의 신기능 — AIO, DirectIO, UUIDv7
데이터베이스가 "블랙박스가 아닌 투명한 엔진"임을 확인하는 여정.
"Redis는 원래 저를 위한 도구였습니다. 1만 명이 쓰기 시작하고, 100만 명이 쓰기 시작했을 때도, 저는 여전히 '내가 쓰기 좋게' 만들려고 했어요. 그게 Redis의 비밀입니다." — Salvatore Sanfilippo (2025, Redis 복귀 인터뷰)
Redis Internals & Distributed Cache — Single Thread, Data Structures, Cluster, Sentinel, RDB/AOF, Redlock, Valkey, Dragonfly Deep Dive (2025)
"Redis is what happens when a C programmer falls in love with data structures." — Salvatore Sanfilippo (antirez)
Few systems have a gap as large as Redis between "just using it" and "really understanding it." Most developers use only GET/SET, and only as a string cache. But Redis is really an In-Memory Data Structure Server — caching is just a byproduct.
Salvatore Sanfilippo built it in 2009 for his analytics tool LLOOGG. MySQL couldn't maintain realtime rankings, so he wrote his own "Remote Dictionary Server." VMware hired him in 2010, then Pivotal (2013), then Redis Labs (now Redis Inc) in 2015. Then in March 2024, Redis abruptly abandoned its open-source license. The Linux Foundation forked Valkey, opening a new front in the cloud wars.
This article is a map for those who want to really understand Redis.
1. Why Redis Is Fast — The Single-Thread Paradox
The Common Misunderstanding
"Single-threaded yet fast? Isn't that wasting cores?"
No. Since Redis 6.0, network I/O is multi-threaded, but command execution remains single-threaded. And that's why it's fast.
The Logic of Single-Thread
- Memory speed approaches CPU speed — bottleneck shifts to network and syscalls
- No locks — zero mutex overhead on shared structures
- No context switches — maximum CPU cache locality
- Atomicity is free — every command is atomic
- Debuggable — bugs are reproducible
antirez: "Even in 2009 I knew lock-based multithreading was hard in practice. Let's make something fast without locks."
But How 1M QPS?
A single Redis instance routinely exceeds 1 million commands per second. The tricks:
- I/O multiplexing —
epoll(Linux) /kqueue(BSD) event loop watching thousands of sockets from one thread - RESP protocol — simple text-based, minimal parsing cost
- Pipelining — batch multiple commands, receive bundled responses
- Not zero-copy, but units are small enough
- Mostly or data structures
One Event Loop Iteration
1. epoll_wait() — find ready sockets
2. Parse request (RESP)
3. Execute command (single thread, memory-only)
4. Write response buffer
5. Move to next event
If one command takes long? Everything stalls. Hence KEYS * and FLUSHALL are forbidden in production. Use SCAN and UNLINK instead.
2. Data Structures — The Real Power
Run OBJECT ENCODING mykey and you'll be surprised. The same "String" is stored as int for numbers, embstr for short, raw for long.
9 Core Data Structures
| Structure | Use Case | Internal | Notes |
|---|---|---|---|
| String | strings/numbers/binary | SDS | up to 512MB |
| List | queue/stack | QuickList | bidirectional O(1) push/pop |
| Hash | object fields | listpack/hashtable | ziplist optimization for small hashes |
| Set | unique values | listpack/intset/hashtable | intset if all integers |
| Sorted Set | rankings/priority queue | Skip List + hash | historically interesting choice |
| Stream | event log | Radix Tree | Kafka-like consumer group |
| HyperLogLog | cardinality estimation | fixed 12KB, 0.81% stddev | probabilistic |
| Bitmap | bit array | String-backed | 1B user DAU in 128MB |
| Geospatial | location queries | Sorted Set + Geohash | GEOADD/GEORADIUS |
SDS — Why Not C Strings
struct sdshdr {
int len; // O(1) strlen
int free; // slack (fewer reallocs)
char buf[]; // data + '\0'
};
- O(1) strlen (C strings are O(N))
- Binary-safe (embedded
\0OK) - Fewer reallocations (2x buffering)
Why Skip List for Sorted Set
antirez explained on his blog:
- Simpler implementation — half the code of B-Tree/Red-Black
- Range query friendly — sorted linked list structure
- Decent memory locality
- Easy to debug — no tree rotations
"I chose Skip List not because it's optimal, but because it's simple to implement." — antirez
HyperLogLog — 12KB for Billions
- "How many unique visitors today?" → Set would explode memory
- HLL is probabilistic — 0.81% stddev
- Fixed 12KB: 100M users or 10B users, still 12KB
PFADD visitors user:1 user:2 user:3
PFCOUNT visitors
PFMERGE today yesterday
Bitmap — Power of SETBIT
SETBIT user:active:20260415 12345 1
BITCOUNT user:active:20260415
BITOP AND weekly user:active:*
1B users → 1B bits → 128MB. Enables "users active every day for 30 days" queries faster than any RDB.
Stream — Kafka-Lite (2018)
Redis 5.0 added Streams with consumer groups and offsets. But persistence depends on Redis, so treat it as a lightweight message bus, not a Kafka replacement.
3. Persistence — RDB vs AOF Tradeoffs
RDB (Redis Database) — Snapshots
- Periodic full dump to binary file
BGSAVE— fork(); child dumps, parent keeps serving- Copy-on-Write prevents parent memory doubling (usually)
- Con: data after fork point may be lost
- Pro: fast restart, small file
AOF (Append-Only File) — Log
- Append every write to file
- fsync policy:
always— per command (slow, no loss)everysec— per second (default, max 1s loss)no— OS-managed (fast, large loss)
- AOF Rewrite — periodic compaction
- Con: slow restart, large file
- Pro: near-zero loss
Hybrid — Redis 4.0+
aof-use-rdb-preamble yes — RDB at the head, incremental AOF at the tail. Fast restart + minimal loss. De facto standard.
Decision Matrix
| Scenario | RDB | AOF | Hybrid |
|---|---|---|---|
| Cache | OK (no persistence needed) | X | X |
| Session store | X | OK (everysec) | OK |
| Fast restart priority | OK | X | OK |
| Primary store | X | OK (always) | OK (best) |
| Disk I/O sensitive | OK | X | careful |
Can You Use It as Primary Storage?
Not recommended. antirez warned repeatedly. Redis targets "fast cache + minimal loss," not "perfect durability." Put important data in Postgres/MySQL; use Redis as cache.
4. Redis Cluster — The Art of Hash Slots
Why Cluster?
- Single Redis has memory/throughput limits (tens of GB per instance)
- Multi-shard needed → Redis Cluster (3.0, 2015)
16384 Hash Slots
- Hash key with CRC16, modulo 16384 (2^14)
- Each slot assigned to a master
- 3 masters → ~5461 slots each
Why 16384
From antirez's famous GitHub issue reply:
- Slot bitmap sent in gossip: 16384 bits = 2KB
- 65536 would be 8KB — too large for gossip
- Below 1000 nodes, 16384 gives enough distribution quality
MOVED & ASK — Redirection
MOVED <slot> host:port— "this slot lives there permanently"ASK <slot> host:port— "migrating now; ask there once"
Smart clients (Lettuce, redis-py-cluster) cache the slot map and refresh on MOVED.
Hash Tag — Same-Slot Guarantee
SET {user:1}:profile "..."
SET {user:1}:sessions "..."
Only the part inside {} is hashed. Essential for MULTI/EXEC, SUNION, Lua scripts touching multiple keys.
Gossip & Failure Detection
- Nodes exchange PING/PONG
- Beyond
cluster-node-timeout→PFAIL(subjective) - Majority PFAIL reports →
FAIL(objective) - Replica auto-promotes
Limitations
- Multi-key commands (SINTER) require same slot
- No cross-slot transactions
- Backups are complex (per-node)
5. Sentinel — Another Path to HA
Cluster bundles sharding and HA. Sentinel provides HA only.
Structure
- 1 master + N replicas + M Sentinels (3+, odd)
- Sentinels monitor master health
- On failure, Raft-like consensus elects new leader
- Clients ask Sentinel for current master
Cluster vs Sentinel
| Aspect | Sentinel | Cluster |
|---|---|---|
| Sharding | X | OK |
| HA | OK | OK |
| Config complexity | Low | Medium |
| Client support | Broad | Smart client required |
| Scale | small–medium | medium–large |
| Multi-key commands | Full | Hash Tag required |
Rule of thumb: fits in one node's memory → Sentinel; otherwise → Cluster.
6. Cache Patterns — The Hot Potato
Cache-Aside (Lazy Loading)
def get_user(id):
user = redis.get(f"user:{id}")
if user is None:
user = db.query(id)
redis.set(f"user:{id}", user, ex=3600)
return user
- Most common
- Pros: simple, DB only hit on miss
- Cons: cold first request, possible stale data
Write-Through
def update_user(id, data):
db.update(id, data)
redis.set(f"user:{id}", data, ex=3600)
- Updates DB + cache on every write
- Good consistency
- Cons: write latency increases, caches cold data too
Write-Behind (Write-Back)
def update_user(id, data):
redis.set(f"user:{id}", data)
queue.push({"id": id, "data": data})
- Minimal write latency
- Cons: data-loss risk, complex
Refresh-Ahead
- Async refresh of near-expiry cache
- Prevents Thundering Herd (below)
Production Mix
Most real systems: Cache-Aside + TTL + (conditional) Write-Through. TTL strategy matters:
- Short TTL (1–5 min) — freshness matters
- Long TTL (1 hr+) — rarely changes
- Jitter —
ex=3600 + random(-300, 300)— avoid synchronous expiry
7. Thundering Herd & Cache Stampede
The most common, subtlest Redis failure cause.
Scenario
- Hot key expires
- Thousands of concurrent requests miss cache
- All hit the DB → DB overload → full outage
Fix 1 — Mutex Lock
def get_with_lock(key):
value = redis.get(key)
if value is not None:
return value
lock = redis.set(f"lock:{key}", "1", nx=True, ex=10)
if not lock:
time.sleep(0.05)
return redis.get(key)
try:
value = db.query(...)
redis.set(key, value, ex=3600)
return value
finally:
redis.delete(f"lock:{key}")
Fix 2 — Probabilistic Early Expiration (XFetch)
Refresh before expiry with probability. Paper: "Optimal Probabilistic Cache Stampede Prevention."
def fetch(key, beta=1.0):
value, ttl, delta = redis.get_with_meta(key)
now = time.time()
if value is None or now - delta * beta * math.log(random.random()) >= ttl:
value = db.query(...)
redis.set(key, value, ex=3600)
return value
Fix 3 — Dual TTL (Soft & Hard)
- Past soft TTL: background refresh, return stale
- Past hard TTL: synchronous refresh
8. Distributed Locks — The Redlock Debate
SET key value NX PX 10000 makes a tempting lock.
Single-Instance Lock
SET lock:resource unique_id NX PX 30000
EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:resource unique_id
Redlock — 5-Instance Distributed Lock
antirez's algorithm:
- Attempt lock on 5 independent Redis instances
- Acquired if majority (3) succeed and total time
<TTL - On failure, release all
Martin Kleppmann's Critique
The DDIA author rebutted famously:
- No fencing token — GC pause can hold expired lock
- Clock-sync assumption weak — NTP jumps, VM pauses break TTL
- If correctness matters, don't use Redlock — use ZooKeeper or etcd
antirez's Counter
- Redlock is for performance — tolerable occasional double-exec
- For correctness, use DB transactions
- Fencing tokens are implementable (monotonic counter)
Practical Conclusion
| Purpose | Tool |
|---|---|
| Dedup (performance) | Redis SET NX PX |
| Leader election (correctness) | ZooKeeper/etcd |
| Transactions | DB itself |
| Money/payments | Never Redis locks |
9. The 2024 License Drama and Valkey Fork
On March 20, 2024, Redis Inc announced:
- Redis 7.4+ switches BSD → SSPL/RSALv2 dual license
- Aimed at cloud providers (AWS ElastiCache etc.)
- Open-source community was outraged
Valkey — Counter-Strike in 48 Hours
- March 28: Linux Foundation launches Valkey
- Founding sponsors: AWS, Google Cloud, Oracle, Ericsson
- Forked from Redis 7.2.4, stays BSD 3-Clause
- Most core maintainers moved to Valkey
antirez Returns
November 2024, antirez rejoins Redis Inc. In 2025 focuses on vector search (RedisVL) and AI.
2025 Landscape
| Product | License | Owner | Contributors |
|---|---|---|---|
| Redis | SSPL/RSALv2 | Redis Inc | antirez returned |
| Valkey | BSD 3-Clause | Linux Foundation | AWS/Google/Oracle |
| KeyDB | BSD 3-Clause | Snap | multi-thread fork |
| Dragonfly | BSL/Apache 2.0 | Dragonfly Labs | rewrite from scratch |
Guide:
- Public cloud managed → doesn't matter (ElastiCache moving to Valkey)
- Self-hosted + open source purist → Valkey
- Multi-thread extremes → Dragonfly
- Need Redis 7.4+ features → Redis
10. Dragonfly — "25x Faster Than Redis"
Launched 2022, rewritten from scratch in C++20. Secrets:
- Multi-thread shared-nothing — each thread owns a shard, no locks
- io_uring — Linux's modern async I/O (faster than epoll)
- Dash hashtable — cache-friendly, based on academic paper
- 30x faster RDB save — novel snapshot algorithm
Performance (2025)
- Single c7g.16xlarge: 6.5M QPS (Redis ~200K)
- 30% better memory efficiency
Limits
- Limited Lua scripting
- Cluster protocol not 100% compatible
- Some edge-case commands missing
- Small ops tooling ecosystem
KeyDB
- Snap's multi-thread fork, ~100% compatible
- Development slowed post-2024; momentum moved to Dragonfly
11. Memory Management — 8 Ways to Avoid OOM
Redis OOM = immediate outage.
maxmemory + Eviction Policy
maxmemory 4gb
maxmemory-policy allkeys-lru
Policies:
noeviction— reject writes (default, risky)allkeys-lru— LRU across allvolatile-lru— LRU among TTL keysallkeys-lfu— LFU (4.0+, recommended)volatile-ttl— nearest TTL firstallkeys-random/volatile-random
For caching, allkeys-lfu is preferred. LRU is weak against scan attacks.
Profiling
MEMORY USAGE user:1
MEMORY STATS
MEMORY DOCTOR
Big Key
Single key > 1MB is dangerous:
- KEYS scan cost
- Network latency
- Full move on cluster resharding
- Fix: split into hash/list
Hot Key
Single key hogs one CPU core:
- Fix: client-side cache, shard, read from replicas
TTL Strategy
- Always set TTL — infinite keys leak memory
- Add jitter — avoid synchronized expiry
- Hot keys longer TTL, cold shorter
12. Lua Scripting & Functions
Redis embeds Lua 5.1, executes multi-command atomic blocks.
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
else
return 0
end
Functions (Redis 7.0+)
Scripts stored as server-side libraries, managed per function.
Caveats
- Lua blocks all other commands — no long scripts
- For hot scripts use
SCRIPT LOAD+EVALSHA
13. Client-Side Caching (Tracking) — Redis 6.0
Redis pushes invalidation to clients that hold local caches.
CLIENT TRACKING ON REDIRECT 1234 BCAST PREFIX user:
- Removes roundtrips → ultra-low latency
- Supported by Lettuce, redis-py, most modern drivers
- Requires client-side memory management
14. Monitoring — 10 Must-Watch Metrics
| Metric | Description | Alert |
|---|---|---|
used_memory / maxmemory | memory usage | 80% |
evicted_keys | evictions | trending up |
connected_clients | concurrent conns | > 10K watch |
instantaneous_ops_per_sec | QPS | vs baseline |
latency_percentiles_usec_* | p99 latency | > 1ms |
rejected_connections | rejections | 0 |
keyspace_hits / misses | hit ratio | > 90% |
aof_current_size | AOF size | disk headroom |
rdb_last_save_time | last RDB | stale = warn |
master_link_status (replica) | repl link | up |
Slow Log
CONFIG SET slowlog-log-slower-than 10000
SLOWLOG GET 10
Never MONITOR
MONITOR streams every command → 50%+ perf drop. Forbidden in production. Use SLOWLOG, LATENCY instead.
15. Anti-Patterns Top 10
- Mass insert without TTL — memory leak
KEYS *orFLUSHALL— single-thread stall- Giant Hash/List accumulation — second-long commands
- Only-in-cache data — unrecoverable on loss
- Same TTL for all keys — simultaneous expiry → stampede
- Redis lock for money — see Redlock debate
- Pub/Sub as durable queue — messages vanish, use Streams
- Write-Through with short TTL — pointless double work
- Writing to replicas — async replication, data loss
- Cross-slot transactions on cluster — silent failure
16. Sensible Redis Checklist
- Is it cache or storage? Be explicit
- Set
maxmemoryand eviction policy explicitly - TTL on every key with jitter
- AOF + everysec for 1s loss budget, consider hybrid
- Ban
MONITOR/KEYS */FLUSHALL - Alert on Big Key / Hot Key (1MB / 10K QPS)
- Target 90%+ hit rate; investigate misses
- Thundering Herd plan — mutex or XFetch
- Distributed lock is performance-only
- Decide Cluster vs Sentinel by data size
- Evaluate client-side caching for read-heavy workloads
- Practice replica-promotion (failover drill)
Closing — The Elegance of Single Thread
Redis's success is paradoxical: in an age of parallelism, serving 1M QPS from one thread. Behind it is antirez's philosophy.
"Real engineering isn't making the complex simple; it's designing something simple from the start." — antirez
Like many "legacy" technologies, Redis is deeper than it looks. Data structures, I/O model, persistence, distribution, tradeoffs — every choice has a reason. The 2024 license fight is the old open-source vs commercialization tension erupting; Valkey/Dragonfly's rise shows we've entered an era where "the Redis interface matters more than Redis itself."
Next — PostgreSQL Internals & Query Optimization
If Redis is "the elegance of data structures," PostgreSQL is "the relational DB masterpiece." Next we'll cover:
- MVCC, VACUUM, WAL and replication
- Query planner internals
- B-Tree/Hash/GiST/GIN/BRIN/HNSW (pgvector)
- Partitioning, pgBouncer
- JSONB vs Document DB
- PostgreSQL 18 (2025) new features (AIO, DirectIO, UUIDv7)
Making the database a transparent engine, not a black box.
"Redis was originally a tool for me. Even when 10K, then 1M people used it, I kept making it 'easy for me to use.' That's Redis's secret." — Salvatore Sanfilippo (2025 return interview)