
你系统平时飞快,某热门商品缓存过期的那一秒,数据库突然被打满、响应全变慢?这很可能就是缓存击穿。Redis 缓存击穿,指的是某个极热 key 在失效瞬间,大量请求同时穿透缓存直击数据库。本文用最小示例讲清三种应对思路、怎么选型,以及它和缓存雪崩的区别。看完你能对症下药。今天这篇文章,编程狮就把这块讲透。三种思路从互斥锁到逻辑过期再到降级兜底,并讲清它和雪崩、穿透的边界,照着选就不会用错方案。
一、先看结论
| 思路 | 做法 | 适用场景 |
|---|---|---|
| 互斥锁(分布式锁) | 只放一个请求回源,其余等待 | 单 key 极热、数据较贵 |
| 逻辑过期 / 永不过期 | 不真删,后台异步刷新 | 命中率优先、可短暂旧数据 |
| 互斥 + 降级兜底 | 锁失败返回旧值或默认值 | 容忍短暂不一致 |

二、击穿到底是什么
Redis 缓存击穿 的关键是“单点”:不是大量 key 同时失效(那是雪崩),而是某一个被高频访问的 key 刚好过期,此时所有请求发现缓存没数据,一窝蜂去查数据库。因为只有一个 key 热,数据库平时靠缓存挡着毫无压力,突然被这一波穿透直接打挂。
一个比喻:收费站平时靠 ETC(缓存)快速过车,偏偏最忙的那条道的 ETC 设备故障(key 失效),所有车改走人工窗口(数据库),窗口瞬间瘫痪。区别它和缓存雪崩:雪崩是“很多车道同时坏”,击穿是“最忙的那一条坏了”。建议先过一遍 Redis 教程 的缓存章节。
三、思路一:互斥锁,只放一个请求回源
import redis, threading
r = redis.Redis()
def get_with_lock(key):
data = r.get(key)
if data is not None:
return data # 缓存命中,直接返回
# 没命中:尝试加锁,只有一个线程能回源
if r.set(f"lock:{key}", "1", nx=True, ex=3):
try:
data = db_query(key) # 查数据库(伪代码)
r.set(key, data, ex=300) # 重建缓存
finally:
r.delete(f"lock:{key}") # 释放锁
return data
else:
# 没抢到锁:稍等重试,或返回旧/默认
return r.get(key) or "默认"
上面这段做了什么:缓存未命中时,用 set nx 抢一把短期锁,只有抢到的那个请求去查库并重建缓存,其余请求等锁释放后自然命中新缓存。这样数据库只承受一次回源压力。注意锁要带超时(ex=3),防止抢到锁的线程崩溃后锁永不释放。
四、思路二与三:逻辑过期与降级兜底
# 思路二:逻辑过期——物理上不设置 key 的 TTL,值里带 expire 字段
# 读时判断:若“逻辑过期”则异步刷新,本次仍返回旧值(业务可接受短暂旧数据)
def get_logical(key):
item = r.get(key)
if item and json.loads(item)["expire"] < time.time():
async_refresh(key) # 后台线程去查库重建
return item
# 思路三:互斥 + 降级——抢不到锁直接返回兜底值,保护数据库
def get_fallback(key):
if r.get(key):
return r.get(key)
if not r.set(f"lock:{key}", "1", nx=True, ex=3):
return "降级默认值" # 宁可返回旧/默认,也不打数据库
# ... 正常回源 ...
上面这段做了什么:思路二让 key“永远在”,只是值里标记逻辑过期时间,读时如果发现过期就甩给后台刷新,本次仍返回旧数据,用户无感知、数据库零压力,代价是可能短暂读到旧值。思路三在锁竞争失败时直接返回兜底,把“保护数据库”放在第一位。
五、两个最容易踩的坑
| 现象 | 原因 | 修复 |
|---|---|---|
| 锁没释放,后续全阻塞 | 抢到锁的线程异常退出,没走 finally 删锁 | 锁必须带 ex 超时 + try/finally |
| 把击穿当成雪崩处理 | 只给所有 key 加随机 TTL,单点热 key 仍会集中失效 | 先区分:单 key 失效用Redis 缓存/逻辑过期 |
第一个坑:分布式锁忘了设超时或异常分支没删锁,会导致缓存长期无法重建。务必 ex 超时兜底 + finally 释放。第二个坑是概念混淆:雪崩靠“错开过期时间”解决,但单点热 key 错开也没用(它本来就会到期),得用互斥锁或逻辑过期针对“单 key”处理。先分清是哪一种,再动手。
先把概念钉死:缓存击穿指的是“某一个特别热点的 key 在过期瞬间,恰好涌入大量并发请求,这些请求发现缓存没值,全部打到数据库,把数据库瞬间压垮”。它和另外两个词容易混:缓存雪崩是“大量 key 在同一时间集体失效”,缓存穿透是“查询一个根本不存在的数据,缓存和数据库都没有,每次都穿透到库”。击穿的特殊性在于,它不是所有 key 失效,而是“偏偏最热的那一个”失效,危害却很集中,因为热点 key 的 QPS 最高。
应对击穿主流有三种办法。其一,互斥锁:第一个发现缓存失效的线程去抢一把锁(Redis 的 SETNX 或语言里的分布式锁),只有抢到的线程去查数据库并回写缓存,其他线程等待或短暂重试;好处是数据库只被捶一次,坏处是实现稍复杂、锁失效要兜底。其二,逻辑过期:缓存的 value 里额外存一个过期时间戳,不依赖 Redis 自身的 TTL,发现“逻辑上过期”时同样用锁触发异步重建,期间仍返回旧值,用户无感、数据库压力小。其三,熔断降级:热点 key 失效且数据库压力大时,直接返回默认值或走限流,宁可少给点数据也别把库打死。
工程上还有两个便宜又好用的补充手段。一是“热点 key 永不过期或临近过期时主动续期”:对极少数顶级热点,干脆不设 TTL,或用一个后台任务在过期前悄悄刷新,从根上避免击穿窗口。二是“提前预热”:大促或发布前,把预计会爆的 key 先算好写进缓存,别等流量来了现查。把击穿、雪崩、穿透三套方案按场景组合——互斥锁管单点热点、随机 TTL 错峰管雪崩、空值缓存管穿透——你的缓存层才算既快又稳。
落地时先配置好 Redis 与编程语言客户端,把互斥锁示例运行起来,检查缓存重建是否符合预期;若数据库仍被打满,按“锁未释放”的方向排查失败原因并修复。方案边界与适用场景根据官方文档整理,未在本机逐语言执行。
总结
要点带走:
- 缓存击穿 是“单个热 key 失效瞬间”的穿透,区别于大量 key 同时失效的“雪崩”;
- 三种思路:互斥锁(单请求回源)、Redis 哨兵(异步刷新、返回旧值)、降级兜底(保护数据库);
- 锁一定要带超时并在 finally 释放,且先分清击穿/雪崩再选方案。
下一步建议系统过一遍 Redis 教程,把缓存、持久化和高可用一起学。
延伸学习
想把这块知识系统补齐,可以按这个顺序来:
- Redis 详解
- Redis 数据保护
- Redis 入门课程
常见问题
Q:缓存击穿和缓存雪崩怎么区分?
A:击穿是“某一个极热 key 失效瞬间”大量请求穿透到数据库;雪崩是“大量 key 在同一时间集中失效”导致整体缓存失效。前者针对单点、用互斥锁或逻辑过期,后者针对批量、用错开过期时间解决。
Q:互斥锁为什么必须带超时?
A:如果抢到锁的线程中途崩溃、没执行到删除锁的代码,这把锁会一直存在,缓存永远无法重建。给锁设一个较短的 ex 超时,即使没正常释放也会自动过期,是兜底保命手段。
Q:逻辑过期返回旧数据会不会出问题?
A:取决于业务。对“商品详情、排行榜”这类容忍短暂不一致的场景完全可用,用户基本无感;但对“账户余额、库存”这类强一致需求,不能用逻辑过期,必须走互斥锁回源拿最新值。

TRAE-AI编程



