--- 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 { 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) { await db.users.update(id, patch); await redis.del(`user:${id}`); } ``` ### Cache stampede (lock 으로 방어) ```ts async function withLock(key: string, ttlMs: number, fn: () => Promise): Promise { 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]]