缓存击穿、缓存穿透与缓存雪崩
本文简要说明三类常见缓存问题的含义、成因与常见解决方案,侧重于实战可用的防护思路。
一、缓存击穿
缓存击穿是指:请求访问的数据在缓存中不存在,但在数据库中存在(注意:这点与「缓存穿透」不同,后者是缓存和数据库中都不存在)。通常由缓存过期引起。
当某个 key 是热点(hot key)时,如果大量并发请求在缓存失效的瞬间同时落到后端数据库,会导致数据库压力骤增,甚至宕机——这就是缓存被“击穿”的场景。
常见解决方案:
互斥锁(只放行一个请求构建缓存)
- 利用分布式锁(例如 Redis 的
SETNX或SET key val NX PX <ttl>)或其他锁机制,仅允许一个请求访问数据库并构建缓存,其他请求等待或返回缓存空值。
- 利用分布式锁(例如 Redis 的
后台续命(主动刷新)
- 对于已知的热点 key,可在缓存到期前由后台任务(或单独线程)主动从数据库刷新缓存。例如:对设置了 10 分钟 TTL 的
hotkey,在第 8~9 分钟时预先刷新并重置 TTL,避免在失效瞬间出现高并发打到数据库。
- 对于已知的热点 key,可在缓存到期前由后台任务(或单独线程)主动从数据库刷新缓存。例如:对设置了 10 分钟 TTL 的
永不过期(适用于稳定不变的热点)
- 如果业务能保证某些 key 的值在长时间内不会变化,并且访问很频繁,可以选择不设置过期,从而避免因过期引起的并发回源。但要谨慎使用(需考虑数据一致性与内存消耗)。
二、缓存穿透
缓存穿透是指:请求的数据在缓存中不存在,且数据库中也不存在。如果攻击者或错误客户端短时间内发送大量此类不同 key 的请求,会把压力全部打到数据库上。
这是常见的恶意请求或异常行为造成的问题。常见应对方式:
缓存空对象
当数据库没有命中时,也将查询结果(如
null或空对象)写入缓存,设置较短的 TTL。下次相同请求就会命中缓存,避免再次打到数据库。这种方式实现简单、成本低。注意:如果攻击请求中的 key 都是不同且仅请求一次,缓存空对象的效果会受限(每个 key 仍然需要一次后端查询)。
布隆过滤器(Bloom Filter)
将所有合法的 key 构建到布隆过滤器中,查询时先经过布隆过滤器判断:若布隆过滤器判断“不存在”,则可以直接拦截请求,不去查数据库;若判断“可能存在”,再去数据库验证。这能挡掉绝大部分非法请求。
布隆过滤器的缺点:
- 不支持删除(需要重建以控制误判率)。
- 对缓存局部性不友好,查询可能跨越大内存空间,影响 CPU 缓存命中率,从而使查询性能下降。
布谷鸟过滤器(Cuckoo Filter)
- 布谷鸟过滤器是布隆过滤器的一种替代方案,支持删除并在某些场景下具有更好的空间/性能折衷,可作为更优选项之一。
三、缓存雪崩
缓存雪崩是指:大量不同的 key 在短时间内同时过期,导致大量请求回落到数据库或后端服务,产生剧烈的流量冲击。
这与缓存击穿不同:缓存击穿通常是大量请求集中访问同一条数据,而缓存雪崩是大量不同数据同时失效。
常见缓解策略:
错峰过期(随机化 TTL)
- 在设置过期时间时加入小幅随机化(例如在基础 TTL 上加上 0~30 秒的随机值),避免大量 key 在同一时刻同时失效。
缓存预热
- 在系统启动或某些维护后,预先将已知热点数据写入缓存,降低冷启动时对数据库的冲击。
多级缓存 / 降级策略
- 使用本地缓存+分布式缓存的组合,或在后端压力过大时返回降级内容(如部分数据、错误提示或静态页面),保护核心服务可用性。
题外:Redis 宕机后的恢复小贴士
- 重启实例前,先在入口层(例如 Nginx 或网关)做流量隔离,将大部分请求指向临时错误页或限流,从而避免新启动的后端被瞬时流量打垮。
- 可在 Redis 重启后做缓存预热,先把已知热点 key 写回缓存,再对外提供服务,避免“重启 → 缓存未命中 → 大量请求打到 DB”的恶性循环。
示例:一个简单的 Nginx 限流/维护页配置片段(仅示意):
server {
listen 80;
server_name example.com;
# 在维护时返回 503 页面
location / {
return 503;
}
error_page 503 /maintenance.html;
location = /maintenance.html {
root /usr/share/nginx/html;
}
}
延伸阅读
参考与致谢
本文参考并借鉴了以下文章的结构与部分表述,特此致谢:
- 孙铭(xm66),《缓存一致性与缓存策略详解》(博客园),原文: https://www.cnblogs.com/xm66/p/15196745.html