Redis 缓存穿透与雪崩的具体防线
港旧胶片
5 min read
判断先说:缓存系统的三大经典故障——穿透、击穿、雪崩,名字相近但成因不同。穿透是查根本不存在的数据绕过了缓存,雪崩是大量 key 同时失效把压力全部砸回数据库,击穿是单个热点 key 失效瞬间被踩踏。各自的防线不同,混淆它们只会让方案无效,甚至在一个问题上堆了另一个问题的解法。
缓存穿透:查一个永远没有的 key
请求的参数在数据库里也查不到,于是缓存里没有、数据库也没有,每次请求都穿透到数据库。若有人用大量不存在的 id 轮询,数据库就会被无意义查询拖垮,且这种流量不会因"多加缓存"而缓解,因为它每次都 miss 且每次都回源。
两道可用防线:
缓存空值。查不到也往缓存里写一个空标记,并设较短过期时间。这样同一 key 再次请求直接命中空值返回,不再访问数据库。代价是要处理"空值污染"——大量不同不存在的 key 会占满缓存,所以过期时间要短,且最好配合布隆过滤器。
查询 key=999999 不存在
→ 缓存写入 {999999: NULL, ttl: 60s}
→ 后续 60s 内同类请求被拦截在缓存层
布隆过滤器。在访问缓存之前,先用布隆过滤器判断 key 是否存在。它用位数组加多个哈希函数,能以极小空间判断"一定不存在"或"可能存在"。凡是过滤器判定不存在的,直接拒绝,连缓存都不用查,从源头掐掉穿透流量。
请求 key → 布隆过滤器判定"不存在" → 直接返回,不查缓存不查库
判定"可能存在" → 走正常缓存/回源流程
它的缺陷是有误判率——会漏放少数实际存在的 key 到"可能"分支,但不会把存在的判成不存在,所以不影响正确性,只是偶尔放过一次正常请求。配合空值缓存,穿透基本可控。
缓存雪崩:同时失效的连锁崩塌
大量缓存 key 在同一时刻集中过期,或缓存服务整体宕机,瞬间所有请求落到数据库,数据库被打挂后连锁影响上游。它和穿透的区别在于:雪崩时 key 是"真实存在"的,只是同一时刻集体失效,回源洪峰把数据库冲垮。
分散过期时间是第一道墙:给每个 key 的 TTL 加一个随机抖动,避免整齐划一地失效。
基础过期 3600s + 随机(0, 600)s
→ 同一批写入的 key 在 3600~4200s 之间错峰失效,避免集体到点
更稳的做法是逻辑过期加后台续期:key 不设物理 TTL,值里带一个逻辑过期时间戳;读取时发现逻辑过期,则返回旧值并异步触发重建,不让所有请求同步去回源。这样即使重建慢,用户拿到的也是旧数据,不会全部阻塞。服务整体不可用场景则要靠高可用部署(主从、哨兵或多副本)和本地内存兜底来扛,单靠 TTL 策略解决不了"缓存集群全挂"的情况。
缓存击穿:一个热点 key 的瞬时踩踏
击穿夹在两者之间:某个极热点 key 失效的瞬间,海量并发同时发现 miss,一起冲向数据库重建。它不同于雪崩之处在于只针对单个 key,但压力集中,数据库一样会被这一波踩踏打垮。
最直接的解法是互斥重建:只有一个线程去查库并写回缓存,其余线程等待或返回旧值。Redis 里可用 SETNX 抢一把重建锁,抢到的去回源,没抢到的稍等重试。
读 miss → SETNX rebuild_lock 成功者去查库写缓存
失败者短暂等待后重试读缓存(或返回降级值)
另一个思路是热点 key 不过期、由后台任务定期刷新,从根上消除"失效那一瞬"的窗口。适合那些可预期的大热点,例如首页榜单、配置类数据。
回源保护是共同底线
无论哪种故障,终究都要落到"数据库不能被打垮"。限流、熔断、回源排队是对所有缓存失效场景的通用兜底:当数据库请求超过阈值,直接拒绝部分流量或快速失败,保住数据库比保住命中率重要得多。缓存是加速层,它的所有失效方案都该假设"数据库扛不住突发"这一前提,而不是指望数据库在没有任何保护时被洪峰冲击还能存活。
三者的关系与优先级
穿透、击穿、雪崩经常同时出现,但优先级清楚:先堵穿透,因为它会无差别地冲击数据库,且和流量大小无关;再防雪崩,它随数据规模增长必然到来;击穿只在有明确大热点时才需要专门处理。上线前最好用压测模拟"缓存全部失效"的极端场景,看数据库能否在限流熔断保护下存活,而不是假设缓存永远在。这套演练能提前暴露很多回源保护缺失的问题,比上线后救火从容得多。
1 view
0 likes
0 comments