缓存不是「把结果塞进 Redis」就结束。真正要设计的是:谁负责读写、失败怎么降级、热点怎么护住数据库。
Cache-Aside(旁路缓存)
读路径:
async function getPost(slug: string) {
const cached = await redis.get(`post:${slug}`);
if (cached) return JSON.parse(cached);
const post = await db.posts.findBySlug(slug);
if (post) {
await redis.set(`post:${slug}`, JSON.stringify(post), "EX", 300);
}
return post;
}写路径:先更新 DB,再删缓存(或短延迟双删)。用「更新缓存」代替「删除」时要小心并发写脏。
三种故障
- 击穿:热点 key 过期,并发全打 DB → 互斥重建或逻辑过期。
- 穿透:查询不存在的数据反复打穿 → 缓存空值(短 TTL)或布隆过滤器。
- 雪崩:大量 key 同时过期 → TTL 加抖动;多级缓存;依赖隔离。
const ttl = 300 + Math.floor(Math.random() * 60); // 抖动
await redis.set(key, value, "EX", ttl);实践建议
- Key 命名带业务域:
post:{slug}、user:{id}:profile。 - 设定可观测性:命中率、重建耗时、DB QPS。
- 不是所有读都该缓存;先缓存「贵且稳」的查询。
小结
缓存模式选对之后,把故障预案写进运行手册。等促销或热点文章来了再补,通常已经晚了。