互斥锁仅防击穿,不防穿透;穿透需前置拦截:参数校验、布隆过滤器、空值缓存组合使用,否则串行查库仍压垮数据库。

互斥锁本身不能解决空指针穿透,它只防击穿;空指针穿透必须靠空值缓存+参数校验+布隆过滤器组合拦截。直接在 get 返回 null 后加锁重建,等于默认接受了“查不到就打库”的逻辑,反而放大穿透风险。
为什么互斥锁对穿透无效?
缓存穿透的本质是查询一个数据库里根本不存在的 key(比如恶意构造的 user:id=-1、order:sn=aaaaa),Redis 里没,DB 里也没有。此时即使你用 SETNX 加锁,所有线程还是会一起查 DB,发现为空后又一起写空值或直接返回——锁只是把并发查 DB 变成串行查 DB,压力没减,还拖慢响应。
- 穿透场景下,
get返回null不代表“缓存失效”,而代表“数据压根不该存在” - 互斥锁的适用前提是:数据存在,只是缓存暂时缺失(即击穿)
- 若不前置拦截,锁会把 1000 个穿透请求变成 1000 次串行 DB 查询,CPU 和连接池照样被打满
真正该在 get 之前做的三件事
必须把防御前移到缓存读取之前,而不是等 get 返回 null 再处理:
- 对入参做严格校验:
userId必须是正整数、phone符合正则、id长度在合理区间——非法参数直接return,不碰 Redis - 用布隆过滤器(Bloom Filter)预判存在性:初始化时将所有合法
userId加入布隆过滤器;每次请求先bloom.contains(userId),返回false就直接拒绝 - 对已确认“存在但查不到”的边缘 case(如刚注册还没写缓存),才走空值缓存:写入
setex user:123 "" 60,注意 value 用空字符串而非null,避免序列化异常
如果非要加锁,怎么避免自旋空转和死锁?
仅适用于“确认数据存在但缓存刚好过期”的击穿场景,且必须配合 double-check 和有限重试:
- 用
SET key value EX 30 NX做原子加锁,不要用SETNX+EXPIRE两步(有竞态) - 获取锁失败后,休眠
Thread.sleep(50)而非自旋;最多重试 3 次,超时直接降级查 DB(防止雪崩式等待) - 业务线程拿到锁后,必须用独立线程异步重建缓存,自己立即返回旧值或空值(逻辑过期方案),别卡住主线程
- 锁 key 必须带业务前缀和随机后缀(如
lock:user:123:abc892),避免因异常未释放导致永久锁死
最常被忽略的一点:空值缓存的 TTL 一定要短(比如 1–2 分钟),且不能和正常缓存 TTL 对齐。否则大量空值集中过期,会引发“伪雪崩”——看着是穿透,实际是空值缓存集体失效导致的二次穿透。











