缓存击穿先挡峰值
很多技术问题看起来是某个 API 用错了,实际更像一次边界没有提前说清的连锁反应。首页推荐位缓存 10 点整同时过期,几百个请求一起打到数据库,缓存系统没坏,数据库却先扛不住,这种情况并不稀奇:功能表面能跑,真正进入复杂路径后,隐藏假设才开始一个个冒出来。
这篇文章想讨论的不是把缓存击穿防护讲成一套万能口诀,而是把它放回真实工作里看:哪些规则需要提前定,哪些复杂度可以延后,哪些地方一旦偷懒就会变成排查成本。我的判断是,先把边界收住,再谈抽象、性能或体验,通常更稳。
先找热点而不是先加缓存
在首页推荐位缓存 10 点整同时过期,几百个请求一起打到数据库,缓存系统没坏,数据库却先扛不住这个场景里,不是所有 key 都需要复杂保护,热点 key 才会在失效瞬间形成冲击。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。缓存击穿防护如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
从机制上看,热点 key、过期时间、互斥锁、请求合并、 stale 数据和降级不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么强一致会增加源站压力,允许短暂旧数据能显著提升稳定性。
落地时建议先做一件小事:用访问量和回源量识别热点。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:盲目给所有缓存加锁,会增加系统复杂度。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
判断这部分做得好不好,不要只看功能是否跑通,而要看热点 key 过期时只有少量请求回源,其余请求有可接受兜底。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“先找热点而不是先加缓存”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
图里只保留了和缓存击穿防护直接相关的路径,目的不是画全系统,而是帮助你判断问题应该从哪一层开始拆。
过期时间不要整齐划一
在首页推荐位缓存 10 点整同时过期,几百个请求一起打到数据库,缓存系统没坏,数据库却先扛不住这个场景里,同一批 key 同时过期,会制造人为流量尖峰。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。缓存击穿防护如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
放到“过期时间不要整齐划一”这个小节里看,相关机制并不是背景知识,热点 key、过期时间、互斥锁、请求合并、 stale 数据和降级不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么强一致会增加源站压力,允许短暂旧数据能显著提升稳定性。
落地时建议先做一件小事:为过期时间增加小范围随机抖动。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:固定 10 分钟过期看似规整,实际上会让风险同步。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
在“过期时间不要整齐划一”这里,验收不该只看一句通过,不要只看功能是否跑通,而要看热点 key 过期时只有少量请求回源,其余请求有可接受兜底。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“过期时间不要整齐划一”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
针对“过期时间不要整齐划一”,可以把检查动作落成三项:
先写清本场景里的关键对象:缓存击穿防护。
再标出会影响它的机制:热点 key、过期时间、互斥锁、请求合并、 stale 数据和降级。
最后补上失败时的判断标准:热点 key 过期时只有少量请求回源,其余请求有可接受兜底。
重建缓存要请求合并
在首页推荐位缓存 10 点整同时过期,几百个请求一起打到数据库,缓存系统没坏,数据库却先扛不住这个场景里,第一个请求负责回源,其余请求等待或读旧值,不要一起查数据库。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。缓存击穿防护如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
放到“重建缓存要请求合并”这个小节里看,相关机制并不是背景知识,热点 key、过期时间、互斥锁、请求合并、 stale 数据和降级不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么强一致会增加源站压力,允许短暂旧数据能显著提升稳定性。
落地时建议先做一件小事:用分布式锁或本机 singleflight 思路。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:锁粒度太大会影响正常 key,太小又挡不住热点。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
在“重建缓存要请求合并”这里,验收不该只看一句通过,不要只看功能是否跑通,而要看热点 key 过期时只有少量请求回源,其余请求有可接受兜底。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“重建缓存要请求合并”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
换到“重建缓存要请求合并”这一步,图里只保留了和缓存击穿防护直接相关的路径,目的不是画全系统,而是帮助你判断问题应该从哪一层开始拆。
下面这段代码只表达思路,重点不在复制,而在看清边界放在哪里:
- const cached = await cache.get(key)
- if (cached) return cached
- return await rebuildWithLock(key, () => loadFromDb())
旧数据有时是更好的降级
在首页推荐位缓存 10 点整同时过期,几百个请求一起打到数据库,缓存系统没坏,数据库却先扛不住这个场景里,推荐位、配置、榜单允许短时间 stale,比全部超时更可接受。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。缓存击穿防护如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
放到“旧数据有时是更好的降级”这个小节里看,相关机制并不是背景知识,热点 key、过期时间、互斥锁、请求合并、 stale 数据和降级不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么强一致会增加源站压力,允许短暂旧数据能显著提升稳定性。
落地时建议先做一件小事:明确哪些数据能读旧,最长能旧多久。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:把所有数据都按强一致处理,会把稳定性让给数据库。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
在“旧数据有时是更好的降级”这里,验收不该只看一句通过,不要只看功能是否跑通,而要看热点 key 过期时只有少量请求回源,其余请求有可接受兜底。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“旧数据有时是更好的降级”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
针对“旧数据有时是更好的降级”,可以把检查动作落成三项:
先写清本场景里的关键对象:缓存击穿防护。
在“旧数据有时是更好的降级”里标出会影响它的机制:热点 key、过期时间、互斥锁、请求合并、 stale 数据和降级。
为“旧数据有时是更好的降级”补上失败时的判断标准:热点 key 过期时只有少量请求回源,其余请求有可接受兜底。
换到“旧数据有时是更好的降级”这一步,图里只保留了和缓存击穿防护直接相关的路径,目的不是画全系统,而是帮助你判断问题应该从哪一层开始拆。
监控要看回源比例
在首页推荐位缓存 10 点整同时过期,几百个请求一起打到数据库,缓存系统没坏,数据库却先扛不住这个场景里,命中率高不代表安全,热点过期瞬间的回源峰值更关键。这不是写法洁癖,而是决定问题发生时团队能不能快速定位责任边界。缓存击穿防护如果没有被提前说清,后面的代码、测试和排查都会各自按自己的理解推进。
放到“监控要看回源比例”这个小节里看,相关机制并不是背景知识,热点 key、过期时间、互斥锁、请求合并、 stale 数据和降级不是孤立存在的。它们会在一次真实请求、一次页面切换或一次批处理任务里互相影响。理解这一层之后,就能看出为什么强一致会增加源站压力,允许短暂旧数据能显著提升稳定性。
落地时建议先做一件小事:记录 key 级别的回源次数和重建耗时。这个动作看起来慢,却能把隐藏分歧提前暴露出来。很多线上问题不是因为团队不会写代码,而是因为大家默认的边界根本不是同一个。
这里最容易踩的坑是:只看平均命中率,会错过尖峰事故。它通常不会在第一天爆炸,而是在数据量变大、用户路径变复杂、或者某个下游服务变慢时突然出现。到那时再补规则,成本会高很多。
在“监控要看回源比例”这里,验收不该只看一句通过,不要只看功能是否跑通,而要看热点 key 过期时只有少量请求回源,其余请求有可接受兜底。如果答案仍然含糊,说明设计还停留在感觉层面,需要继续把条件、异常和责任写具体。
在“监控要看回源比例”这一段里,我更愿意把复杂度摊开放到日志、状态和验收规则里,而不是塞进默认行为。这样做不一定显得聪明,但后续排查会更稳:谁触发、谁处理、失败后谁接手,都能在材料里找到依据。
收尾时看这三个信号
第一,看问题能不能被命名。比如这篇里的核心不是泛泛的“优化一下”,而是缓存击穿防护有没有清楚边界。能命名的问题,才容易进入评审、测试和复盘。
第二,看失败能不能被复现。围绕热点 key 过期时只有少量请求回源,其余请求有可接受兜底设计一组小样本,比等线上偶发问题更可靠。样本不需要复杂,但要覆盖正常、异常、边界和恢复。
第三,看团队能不能做出一致选择。强一致会增加源站压力,允许短暂旧数据能显著提升稳定性,这类取舍没有绝对答案,但必须有理由、有记录、有回滚空间。否则今天靠经验放过的点,明天就会变成另一个人看不懂的坑。
真正有价值的工程文章,不是把每个概念都讲满,而是帮读者在下次遇到类似场景时更早地停一下:这件事的边界定了吗,失败路径想过了吗,验收标准能说清吗。只要这三个问题能回答,很多复杂度就已经少了一半。
