缓存不是越多越好,边界和失效策略才是关键。
为什么要关注这个话题
这篇文章面向有专职运维支持的团队,从成本视角深入拆解缓存设计入门。当前定位为「实战」阶段,核心目标是面向真实流量与团队协作。我们会从实际场景出发,结合具体代码示例,把关键知识点拆解为可落地的行动步骤。衡量标准:QPS/成本比。
数据库的 QPS 上限通常在几千到几万,而 Redis 这样的缓存系统可以轻松达到十万级。对于读多写少的场景,缓存能将响应时间从几十毫秒降到亚毫秒级。但缓存引入了数据一致性的挑战:当数据库更新了,缓存里的旧数据怎么办?不同的业务场景对一致性的容忍度不同,这决定了你应该选择哪种缓存策略。
背景与问题
缓存是后端系统中最常用的性能优化手段,但也是最容易出问题的地方。“缓存一时爽,一致性火葬场”这句话虽然夸张,但确实反映了现实。很多线上事故的根因都是缓存数据与数据库不一致,或者缓存雪崩导致数据库被打垮。理解缓存的设计模式和失效策略,是后端工程师的必修课。
在「线上问题频发的阶段」这个阶段,成本问题尤为突出。过度优化导致投入失衡是最容易踩的坑,我们需要先建立正确的度量体系,再逐步优化。
方法论与实践路径
最常用的缓存模式是 Cache Aside(旁路缓存):
读流程:先查缓存,命中则直接返回;未命中则查数据库,将结果写入缓存后返回。 写流程:先更新数据库,再删除缓存(而不是更新缓存)。
为什么是”删除”而不是”更新”缓存?因为在并发场景下,两个写请求可能导致缓存中存储了旧值。删除缓存让下一次读请求自然回源,保证最终一致性。
TTL(Time To Live)是缓存的安全网。即使删除操作失败,TTL 到期后缓存也会自动失效。建议根据业务容忍度设置 TTL:用户信息 5-15 分钟,配置数据 1-5 分钟,热点数据 30-60 秒。
对于一致性要求更高的场景,可以使用延迟双删:先删缓存,更新数据库,等待一小段时间后再删一次缓存,覆盖可能的并发读回填。
代码示例
下面是一个可以直接参考的实战级别示例:
// Cache Aside 读流程
async function getUser(id) {
const key = `user:${id}`;
let data = await redis.get(key);
if (data) return JSON.parse(data); // 缓存命中
data = await db.query('SELECT * FROM users WHERE id = ?', [id]);
if (data) {
// 回填缓存,TTL 5分钟 + 随机偏移防雪崩
const ttl = 300 + Math.floor(Math.random() * 60);
await redis.set(key, JSON.stringify(data), 'EX', ttl);
}
return data;
}
// Cache Aside 写流程
async function updateUser(id, updates) {
await db.query('UPDATE users SET ? WHERE id = ?', [updates, id]);
await redis.del(`user:${id}`); // 先更新库,再删缓存
}
落地建议
实际落地时,还需要考虑缓存穿透(查询不存在的数据)、缓存击穿(热点 Key 过期瞬间大量请求打到数据库)和缓存雪崩(大量 Key 同时过期)。应对方案分别是:布隆过滤器或缓存空值、互斥锁或永不过期+异步更新、TTL 加随机偏移量。监控方面,重点关注缓存命中率(目标 > 95%)和回源 QPS。
常见误区与避坑指南
最常见的错误是”先删缓存再更新数据库”,这在并发下几乎必然导致脏数据。其次是 TTL 设置过长,导致数据不一致的窗口期太大。还有一种情况是缓存了过多的冷数据,占用内存但命中率很低——缓存应该只存热数据。
小结
缓存设计入门的实战阶段,核心是面向真实流量与团队协作。从成本角度出发,关注QPS/成本比,避免过度优化导致投入失衡。把上面的实践清单逐项落地,你会发现效果比想象中来得快。