Files
2nd/10_Wiki/Topics/Coding/DB_Redis_Patterns.md
T
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
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>
2026-05-20 23:52:15 +09:00

5.6 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
db-redis-patterns Redis 패턴 — Cache / Pub/Sub / Streams / Lua Coding draft B conceptual 2026-05-09 2026-05-09
database
redis
cache
vibe-coding
language applicable_to
TS / Redis
Backend
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 (단순)

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 으로 방어)

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)

-- 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
const ok = await redis.eval(rateLimitLua, 1, `rate:${userId}`, 60_000, 100, Date.now());
if (!ok) throw new Error('rate limited');

Sorted set (leaderboard)

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)

await redis.hset('user:42', { name: 'Alice', age: 30 });
const user = await redis.hgetall('user:42');

Pub/Sub (broadcast)

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 (영속 큐)

// 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 참조)

const token = uuid();
const ok = await redis.set('lock:resource', token, 'PX', 30_000, 'NX');
// ... safe release with Lua

HyperLogLog (unique count, 작은 메모리)

await redis.pfadd('uniq:visitors:2026-05-09', userId);
const count = await redis.pfcount('uniq:visitors:2026-05-09');
// 12KB 로 수억 unique 추정 (오차 ~1%)

Bitmap (bit 단위)

// 사용자 N의 작년 365일 출석
await redis.setbit('attendance:42', dayOfYear, 1);
const days = await redis.bitcount('attendance:42');

Geo

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)

const pipe = redis.pipeline();
for (const id of ids) pipe.get(`user:${id}`);
const results = await pipe.exec();
// 한 번 round-trip, N 결과

TTL 항상

// ❌ 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 후속.

🔗 관련 문서