项目概述
本项目为 REST API 增加旁路缓存层:热数据从 Redis 返回,未命中回退数据库,写入时按带版本标签前缀失效。缓存是对“短暂陈旧好过无数据”的下注,只有读路径与失效路径一起设计才划算。
架构与关键决策
get-or-load 助手拥有读路径:先查 Redis,未命中则加载数据库、带抖动 TTL 写回并返回;所有读者经此一处,使 TTL、序列化与回退只有一份实现。单飞映射保证同一键的并发未命中只触发一次加载,冷键在压力下不会放大成数据库踩踏。写入通过提升标签前缀中的版本使相关键不可达而非枚举删除;TTL 带抖动,避免大批键同时到期把稳定命中率变成周期性缺失风暴。
实施与测试
第二次读取断言未调用加载器;写入断言标签失效;同一键十次并发未命中断言只调用一次加载器;断言写出的键集合不共享精确到期时间。
限制与后续
- 仅旁路缓存,无写穿或写回。
- 失效在 TTL 窗口内最终一致。
- 假定单一 Redis,集群部署需考虑键分片。
代码示例
pnpm init
pnpm add ioredis express pg
pnpm add -D typescript tsx vitest
redis-cache-layer/
├── src/cache.ts
├── src/tags.ts
├── src/api.ts
├── src/metrics.ts
├── tests/cache.test.ts
└── package.json
const key = (ns, id, v) => `${ns}:v${v}:${id}`;
export async function getOrLoad(key, ttl, load) {
const hit = await redis.get(key);
if (hit) return JSON.parse(hit);
const value = await load();
await redis.set(key, JSON.stringify(value), 'EX', ttl + Math.floor(Math.random() * 30));
return value;
}
const inflight = new Map();
const p = inflight.get(key) ?? load().finally(() => inflight.delete(key));
inflight.set(key, p);
await redis.sadd(`tag:${tag}`, key);
async function invalidateTag(tag) { const keys = await redis.smembers(`tag:${tag}`); if (keys.length) await redis.del(...keys); }
metrics.hits += 1; // or misses += 1
metrics.observeLoad(ms);
pnpm vitest run