9148c358d0
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
5.6 KiB
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 |
|
|
|
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 후속.