贴吧式
缓存击穿那一刻,谁来兜底:互斥锁、逻辑过期、单飞还是直接放过去?聊聊你们场景下的取舍
帖子正文与楼层回复。
楼主 林墨白-291
缓存击穿高并发性能调优本地缓存单飞模式
背景
社区里已经有几篇把「缓存为什么慢」讲得很透的帖子——比如 @林砚舟 的《Redis 热 key 单分片被打到 98% CPU:本地缓存 + 一致性令牌 + 限流的三层防线》,把根因定位到「分片均衡被单 key 击穿、其余分片空转」,解法是把 90% 的读拦在进程内。我基本认同这套思路。
但真正动手那一刻,我发现团队里吵得最凶的其实不是「要不要缓存」,而是缓存 miss 的那一瞬间、几百上千个并发请求同时涌过来,到底让谁去回填:
常见分歧 / 几种做法
- 互斥锁 + 单飞(single-flight):拿到锁的实例去查 DB 回填,其余请求短暂阻塞或自旋等待。实现最直观,但锁本身(Redis 分布式锁 / 本地 ReentrantLock)的 TTL、续期、误释放都是坑——社区里《分布式锁误释放导致库存超卖》《Sentinel 主从切换让 Redis 锁瞬间蒸发》两篇已经把锁的脆弱性讲得很清楚。
- 逻辑过期:缓存里存一个「逻辑过期时间」,物理 TTL 设很长甚至不设;过期后第一个请求异步起线程重建,其余请求先拿旧值返回。优点是 DB 压力小、读路径不阻塞;缺点是「旧值」在多长时间内可接受,完全取决于业务。
- 本地缓存兜底(进程内 Caffeine / Guava):把热点 key 下沉到内存,TTL 短(秒级),过期后回源。优点是延迟低、不依赖分布式协调;缺点是 N 个实例各自回源 N 次,DB 压力是「实例数 × 击穿次数」,且实例间不一致。
- 限流 + 降级:击穿时直接对回源路径限流,超出的请求走降级(返回默认值 / 旧快照 / 空)。本质是「承认扛不住,主动让一部分请求拿到次优结果」。
- 什么都不做,让 DB 扛:有些团队觉得热点 key 的 DB 查询本身走索引,QPS 1000 级别扛得住,何必加锁加线程?
我的倾向与理由
我个人倾向是:默认用「本地单飞 + 短 TTL」起步,而不是上来就上分布式锁。理由是:
- 分布式锁的失败模式太多(锁过期、续期竞争、主从切换丢锁),而单飞是进程内的,失败模式只有「DB 慢」这一种,可控得多;
- 逻辑过期适合「读多写极少、且旧值可接受 1-5 秒」的场景(比如商品详情、配置项),不适合「强一致」场景(比如库存、余额);
- 限流 + 降级应该是最后一道防线,而不是第一反应——因为它意味着你主动放弃了部分请求的正确性,需要业务方明确签字。
但这些都是「我的场景」,不同业务的取舍差异极大。
想听大家在什么场景下怎么选
特别想收集几个具体的数据点,方便大家对照:
- 你们热点 key 的 QPS 量级大概多少?(几十 / 几百 / 几千 / 上万)
- 回填一次 DB 查询的 P99 延迟是多少?(<5ms / 5-50ms / >50ms)
- 业务方对「旧值可接受的窗口」有没有明确说法?(0ms 强一致 / 1 秒 / 5 秒 / 无所谓)
- 你们实例数是多少?本地单飞会不会导致「N 实例 × N 次回源」把 DB 打穿?
- 踩过最痛的一次「击穿」事故,根因最后落在哪一层?是锁、是 TTL、是 DB 慢、还是别的?
不一定有标准答案,但想看看大家在真实 QPS + 真实 P99 + 真实一致性要求下,最终选的是哪条路、为什么。欢迎贴配置、贴代码、贴事故复盘,越具体越好。
(引用:@林砚舟 的热 key 三层防线帖、@温知远 的两篇分布式锁事故复盘,都是很好的延伸阅读。)
赞 0 · 收藏 0 · 楼层 0
楼层
还没有人回复。