读路径:先缓存,未命中再回源
Cache Aside(旁路缓存)的读路径三步走:先读缓存,命中直接返回;未命中就查库;查到之后顺手写回缓存,再返回。下次同样的请求就在缓存层解决了。
public ItemDetail getItem(Long id) {
String key = "item:detail:" + id;
ItemDetail d = redis.get(key); // 1. 先读缓存
if (d != null) return d; // 命中:直接返回
d = itemDao.loadDetail(id); // 2. 未命中:回源查库
redis.setex(key, 1800, d); // 3. 写回缓存(带 TTL)
return d;
}读路径简单,坑都在写路径。写的时候缓存怎么办?是删掉还是更新?先删还是先更库?四个组合里只有一个是最优解。
为什么是「删」而不是「更新」
直觉上「更新库的时候顺便把缓存也更新」天经地义,实际有两个坑:并发写乱序——线程 A 更新库 x=1,线程 B 更新库 x=2,若 B 的缓存更新先到、A 的后到,缓存里留下 1,库里是 2,从此永远差一截;写放大——缓存值往往是多表数据的聚合结果,写一次库要重算整个聚合,写多了读得少就是白算。
「删除」则把难题变简单:删掉之后,下次读自然回源到最新的库——懒加载哲学,用到才算,不提前算。并发的两个写,谁先删谁后删无所谓,删了就是干净的。
为什么「先更库,再删缓存」
剩下的问题是先后顺序。先删缓存再更库:删完到库更新完成之间有个窗口,并发的读请求会回源到旧库值并写回缓存——脏数据要在缓存里住到 TTL 过期,窗口明显、后果持久。先更库再删缓存:理论上也有缝隙——读请求在缓存未命中后回源拿到了旧值,此时写请求恰好完成更库与删缓存,随后读请求把旧值写回。但这个缝隙要求「读比写还慢地跨过整个写过程」,概率极小,且有 TTL 兜底。
所以标准答案是:先更新数据库,再删除缓存。这不是完美解,是概率与代价权衡后的最优解,下篇还会专门算这笔账。
三个变体,各自的舞台
| 模式 | 做法 | 取舍 |
|---|---|---|
| Cache Aside | 应用自己管缓存读写 | 简单可控,主流选择 |
| Read/Write Through | 缓存层代理读写,应用只面对缓存 | 业务干净,但缓存服务要可靠 |
| Write Behind | 只写缓存,异步批量刷库 | 写性能极高,宕机可能丢数据 |
Write Behind 看着诱人——点赞计数、浏览量这类高频写、偶尔丢一点也无所谓的场景是它的主场;但订单、库存碰都别碰。绝大多数业务用 Cache Aside 就够了,把注意力放到它管不住的地方去:缓存里永远查不到的数据——穿透,下一篇的主角。
评论 (0)