读路径
旁路缓存读取时先查缓存,命中则返回;未命中则从数据库加载并写回缓存。数据库始终是事实来源,应用决定缓存什么、缓存多久以及如何失效。
async function getUser(id: string): Promise<User> {
const key = `user:${id}`;
const cached = await cache.get(key);
if (cached) return JSON.parse(cached);
const user = await db.findUser(id);
if (user) await cache.set(key, JSON.stringify(user), { EX: 300 });
return user;
}
即使有显式失效,也要设置 TTL。TTL 是漏失效、部署缺陷和手工改库的安全网。没有 TTL 的缓存可能在某次遗漏删除后永久返回过期数据。
写路径才是难点
常见建议是先更新数据库、提交事务,再删除缓存键。提交后删除可以避免“读者在事务提交前用旧值重新填充缓存”的竞态。即便如此,并发读者仍可能在提交前读到旧值、在删除后写回。较短 TTL 和带版本号的键可以缩小窗口;要求强一致时应换一种设计。
不要把缓存和数据库当作没有部分失败预案的双写。缓存更新成功而数据库写入失败,会让缓存保存一个从未存在过的值。提交后删除加 TTL 比试图让两次写原子化更容易推理。
保护热点键
热点键过期时可能发生击穿。用 single-flight 合并并发未命中、允许过期数据继续服务并在后台刷新,或给 TTL 加随机抖动。对不存在记录的负缓存应很短,否则刚创建的记录会在一段时间内看起来不存在。
何时使用
当数据读多写少、可以接受短暂未命中,并且应用能在写入时执行失效,使用旁路缓存。优点是简单可控;缺点是每条读路径都要实现该模式,每条写路径都要记得失效。与读穿透缓存相比,旁路缓存让缓存产品与数据库解耦,但把更多逻辑放在应用里。