缓存击穿是热点key过期瞬间大量并发请求穿透至数据库,互斥锁必须基于redis实现(非本地锁),需用set key val nx ex原子设锁、lua脚本校验删除,并配合指数退避重试与合理锁粒度。

缓存击穿不是“缓存没命中的普通情况”,而是热点 key 刚过期那一瞬间,大量并发请求同时穿透到数据库——它专挑高并发+单点失效的时机爆发,不加控制时数据库 QPS 可能瞬间翻几十倍。互斥锁能解决,但直接用 threading.Lock 或简单 SETNX 很容易翻车。
为什么本地锁(如 Python 的 threading.Lock)在 Redis 场景下完全无效
本地锁只在当前进程/线程内起作用。Web 服务通常多进程部署(比如 gunicorn 启多个 worker),或者用异步框架(如 FastAPI + uvicorn 多 worker),此时每个进程都有自己的 threading.Lock 实例,彼此完全隔离。A 进程加了锁,B 进程照样能同时查库——根本没互斥。
必须把锁放在所有进程都能看到的地方,也就是 Redis 本身。
实操建议:
- 锁的 key 要带业务标识,比如
lock:product:10086,避免不同资源共用一把锁 - 永远不用
SETNX单独设锁,必须配合过期时间(EX),否则进程崩溃后锁无法自动释放,变成死锁 - 推荐用原子命令
SET key value NX EX seconds,它一步完成「不存在才设 + 设置过期」,避免竞态
SETNX 加锁后,怎么安全地释放锁?不能直接 DEL
直接 DEL lock:product:10086 是危险操作:如果 A 进程拿到锁、执行慢,超时前还没删锁,而锁已自动过期;此时 B 进程成功获取新锁,接着 A 进程执行 DEL,结果删掉了 B 的锁——后续 C 进程可能和 B 同时操作数据库。
正确做法是「校验再删」:只有自己设的锁值,才能删。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
实操建议:
- 设锁时用唯一值,比如 UUID 或毫秒级时间戳 + 进程 ID,如
"a1b2c3d4-5678-90ef-ghij-klmnopqrstuv" - 删锁必须用 Lua 脚本保证原子性:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - 不要依赖客户端重试逻辑来“等锁释放”,那会放大延迟;应设置合理锁过期时间(一般比查库+写缓存耗时多 2–3 倍)
等待锁失败时,该 sleep 还是轮询?要不要加最大重试次数
空转轮询(比如 while 循环不停 GET)会浪费连接、打满 Redis CPU;盲目 sleep 又可能导致响应毛刺——比如统一 sleep 100ms,但实际锁 5ms 就释放了,白白拖慢 95ms。
更务实的做法是「指数退避 + 随机偏移」。
实操建议:
- 首次等待 10–50ms(加随机避免集体苏醒),失败后翻倍,上限设为 200ms
- 最多重试 3–5 次,之后直接报错或降级返回默认值,防止雪崩传导
- 每次重试前先
GET一次目标 key,如果此时缓存已回填,就立即返回,不用等锁
真正难的不是写对那几行加锁代码,而是想清楚锁粒度是否匹配业务:一个商品详情页用商品 ID 作锁 key 没问题,但如果接口支持批量查 100 个商品,还对每个 ID 单独加锁,就会产生 100 次 Redis 往返和潜在锁竞争——这时候该考虑批量预热或改用逻辑过期方案。锁只是工具,别让它成了新瓶颈。










