f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
211 lines
5.6 KiB
Markdown
211 lines
5.6 KiB
Markdown
---
|
|
id: db-redis-patterns
|
|
title: Redis 패턴 — Cache / Pub/Sub / Streams / Lua
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [database, redis, cache, vibe-coding]
|
|
tech_stack: { language: "TS / Redis", applicable_to: ["Backend"] }
|
|
applied_in: []
|
|
aliases: [Redis, Valkey, cache, pub/sub, sorted set, hyperloglog, Lua script]
|
|
---
|
|
|
|
# Redis 패턴
|
|
|
|
> "내장 자료구조 server". String / Hash / List / Set / Sorted Set / Stream / Bitmap / HyperLogLog / Geo. **단일 thread + atomic command + Lua**. Cache, queue, leaderboard, rate limit.
|
|
|
|
## 📖 핵심 개념
|
|
- 단일 thread → atomic operation 자연.
|
|
- Lua script: 여러 command 가 atomic.
|
|
- Pipeline: 다중 command 한 번에 보냄.
|
|
- TTL: 모든 key 에 만료.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### Cache (단순)
|
|
```ts
|
|
import Redis from 'ioredis';
|
|
const redis = new Redis();
|
|
|
|
async function getUser(id: string): Promise<User> {
|
|
const cached = await redis.get(`user:${id}`);
|
|
if (cached) return JSON.parse(cached);
|
|
|
|
const user = await db.users.find(id);
|
|
await redis.setex(`user:${id}`, 60, JSON.stringify(user)); // 60초 TTL
|
|
return user;
|
|
}
|
|
|
|
// 무효화
|
|
async function updateUser(id: string, patch: Partial<User>) {
|
|
await db.users.update(id, patch);
|
|
await redis.del(`user:${id}`);
|
|
}
|
|
```
|
|
|
|
### Cache stampede (lock 으로 방어)
|
|
```ts
|
|
async function withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T> {
|
|
const lock = `lock:${key}`;
|
|
const ok = await redis.set(lock, '1', 'PX', ttlMs, 'NX');
|
|
if (!ok) {
|
|
await sleep(50); // 다른 process 가 만드는 중 — 잠깐 대기
|
|
const v = await redis.get(key);
|
|
if (v) return JSON.parse(v);
|
|
}
|
|
try {
|
|
const v = await fn();
|
|
await redis.setex(key, 60, JSON.stringify(v));
|
|
return v;
|
|
} finally {
|
|
await redis.del(lock);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Rate limit (sliding window, Lua)
|
|
```lua
|
|
-- rate-limit.lua
|
|
local key = KEYS[1]
|
|
local window = tonumber(ARGV[1])
|
|
local limit = tonumber(ARGV[2])
|
|
local now = tonumber(ARGV[3])
|
|
|
|
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
|
|
local count = redis.call('ZCARD', key)
|
|
if count >= limit then return 0 end
|
|
redis.call('ZADD', key, now, now)
|
|
redis.call('PEXPIRE', key, window)
|
|
return 1
|
|
```
|
|
|
|
```ts
|
|
const ok = await redis.eval(rateLimitLua, 1, `rate:${userId}`, 60_000, 100, Date.now());
|
|
if (!ok) throw new Error('rate limited');
|
|
```
|
|
|
|
### Sorted set (leaderboard)
|
|
```ts
|
|
await redis.zadd('scores', 100, 'alice', 80, 'bob', 95, 'charlie');
|
|
|
|
// Top 10
|
|
const top = await redis.zrevrange('scores', 0, 9, 'WITHSCORES');
|
|
|
|
// Alice rank
|
|
const rank = await redis.zrevrank('scores', 'alice');
|
|
```
|
|
|
|
### Hash (object)
|
|
```ts
|
|
await redis.hset('user:42', { name: 'Alice', age: 30 });
|
|
const user = await redis.hgetall('user:42');
|
|
```
|
|
|
|
### Pub/Sub (broadcast)
|
|
```ts
|
|
const sub = new Redis();
|
|
sub.subscribe('events');
|
|
sub.on('message', (ch, msg) => console.log(msg));
|
|
|
|
// 다른 client
|
|
const pub = new Redis();
|
|
await pub.publish('events', JSON.stringify({ type: 'order.created', id }));
|
|
```
|
|
|
|
⚠️ Pub/Sub = at-most-once. 영속 X — Streams 쓰자.
|
|
|
|
### Streams (영속 큐)
|
|
```ts
|
|
// Producer
|
|
await redis.xadd('orders', '*', 'orderId', 'o1', 'userId', 'u1');
|
|
|
|
// Consumer group
|
|
await redis.xgroup('CREATE', 'orders', 'workers', '0', 'MKSTREAM');
|
|
|
|
while (true) {
|
|
const r = await redis.xreadgroup('GROUP', 'workers', 'me', 'COUNT', '10', 'BLOCK', '5000', 'STREAMS', 'orders', '>');
|
|
// ...
|
|
await redis.xack('orders', 'workers', messageId);
|
|
}
|
|
```
|
|
|
|
### Distributed lock (위 Distributed Locks 참조)
|
|
```ts
|
|
const token = uuid();
|
|
const ok = await redis.set('lock:resource', token, 'PX', 30_000, 'NX');
|
|
// ... safe release with Lua
|
|
```
|
|
|
|
### HyperLogLog (unique count, 작은 메모리)
|
|
```ts
|
|
await redis.pfadd('uniq:visitors:2026-05-09', userId);
|
|
const count = await redis.pfcount('uniq:visitors:2026-05-09');
|
|
// 12KB 로 수억 unique 추정 (오차 ~1%)
|
|
```
|
|
|
|
### Bitmap (bit 단위)
|
|
```ts
|
|
// 사용자 N의 작년 365일 출석
|
|
await redis.setbit('attendance:42', dayOfYear, 1);
|
|
const days = await redis.bitcount('attendance:42');
|
|
```
|
|
|
|
### Geo
|
|
```ts
|
|
await redis.geoadd('places', 127.0, 37.5, 'seoul', 139.6, 35.6, 'tokyo');
|
|
const nearby = await redis.geosearch('places', 'FROMMEMBER', 'seoul', 'BYRADIUS', '2000', 'km', 'ASC');
|
|
```
|
|
|
|
### Pipeline (batch)
|
|
```ts
|
|
const pipe = redis.pipeline();
|
|
for (const id of ids) pipe.get(`user:${id}`);
|
|
const results = await pipe.exec();
|
|
// 한 번 round-trip, N 결과
|
|
```
|
|
|
|
### TTL 항상
|
|
```ts
|
|
// ❌ TTL 없음
|
|
await redis.set(key, value);
|
|
|
|
// ✅ TTL
|
|
await redis.setex(key, 3600, value);
|
|
// 또는
|
|
await redis.set(key, value, 'EX', 3600);
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 사용 | 자료구조 |
|
|
|---|---|
|
|
| Object cache | String (JSON) / Hash |
|
|
| Counter / atomic | INCR / DECR |
|
|
| Recent N | Sorted set (score=ts) |
|
|
| Queue | List (LPUSH/BRPOP) 또는 Streams |
|
|
| 분산 lock | SETNX + Lua |
|
|
| Rate limit | ZSET / Lua sliding window |
|
|
| Pub/sub | Streams (영속) / Pub/Sub (휘발) |
|
|
| Unique count | HyperLogLog |
|
|
|
|
## ❌ 안티패턴
|
|
- **TTL 없는 key**: 메모리 누적.
|
|
- **Big key (10MB+)**: blocking. 작게 / chunk.
|
|
- **KEYS \* prod**: blocking. SCAN.
|
|
- **Pub/Sub 영속 가정**: 잃음.
|
|
- **Cache write 없는 invalidation**: stale.
|
|
- **Lock 없는 cache stampede**: thundering herd.
|
|
- **여러 명령 atomic 가정 (transaction 없음)**: race. MULTI / Lua.
|
|
- **단일 redis 가정 prod**: HA — Sentinel / Cluster.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- TTL + namespace prefix (`user:42` 같은) + atomic Lua.
|
|
- HyperLogLog / Bitmap / Sorted Set 활용 = power.
|
|
- Streams 가 Pub/Sub 의 modern 후속.
|
|
|
|
## 🔗 관련 문서
|
|
- [[Backend_Rate_Limiting]]
|
|
- [[DB_Distributed_Locks]]
|