连载中 2/20

Cache Aside:最经典的读写模式

2026-05-25 · 8257 阅读 · 0 评论 · 0 赞

读路径:先缓存,未命中再回源

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 就够了,把注意力放到它管不住的地方去:缓存里永远查不到的数据——穿透,下一篇的主角。

503

10 年全栈工程师 · 503咖啡馆主理人

#CacheAside#旁路缓存#读写模式#懒加载#WriteBehind

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞