互斥锁方案适合强一致性场景,通过Redisson RLock双重检查加锁,避免缓存击穿导致DB压力激增;逻辑过期方案用expireTime字段实现异步刷新,保障高并发读性能;永不过期+主动更新依赖严格发布规范;二级缓存(Caffeine+Redis)应对Redis故障,需解决多实例一致性;布隆过滤器和空值缓存不适用于击穿问题。

互斥锁方案:适合强一致性要求的热点数据
当某个 product_id=123 的商品在秒杀时缓存刚好过期,大量请求同时打到数据库,redisTemplate.opsForValue().get("product:123") 返回 null,此时必须防止所有请求都去查库。
用 Redisson 的 RLock 是最稳妥的做法:
- 加锁前先查一次缓存(双重检查),避免锁竞争浪费
- 锁超时时间要明显长于数据库查询+写缓存耗时,否则可能多个线程都拿到锁
- 释放锁必须放在
finally块里,否则异常后锁不释放会雪崩 - 锁 key 要带业务标识,比如
"lock:product:123",不能全局共用一个锁
压测显示:未加锁时 DB QPS 瞬间冲到 18500;加锁后稳定在 200 以内。但吞吐量下降约 30%,适合金融、订单类强一致场景。
逻辑过期方案:高并发读 + 容忍短暂不一致
不真正让 key 过期,而是把过期时间作为 value 的一部分存进去,比如存一个 {"data": {...}, "expireTime": 1743796000000} 结构。
查询时先判断 expireTime 是否已过期,若过期则异步刷新缓存,当前请求仍返回旧值。
- 完全避免阻塞,QPS 几乎无损
- 需要额外字段存储逻辑时间,反序列化成本略增
- 异步刷新失败时旧数据会一直存在,需配合监控告警(如连续 3 次刷新失败触发告警)
- 不适合价格、库存等对实时性极度敏感的数据
电商详情页常用此法,用户刷两次看到价格微调,比卡顿或报错体验更好。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
永不过期 + 主动更新:适用于变更频率低的热点数据
像“平台公告”“系统配置”这类数据,本身就不该设 TTL,而是由写操作主动触发 redisTemplate.delete("system:config") 或 set 新值。
- 彻底消灭击穿可能,零额外开销
- 依赖业务方严格保证“有更新必删/写缓存”,漏掉一次就永久脏数据
- 需配套发布流程校验,比如 CI 阶段扫描所有
updateConfig()方法是否含缓存操作 - 不适用于 DB 直接被其他服务修改的场景(如跨系统共享 MySQL)
这个方案表面最简单,实际对工程规范要求最高——它把风险从运行时转移到了开发和发布环节。
二级缓存(Caffeine + Redis):本地缓存兜底防穿透式击穿
当 Redis 集群瞬时不可用(如网络分区),仅靠 Redis 的互斥锁或逻辑过期都会失效。这时本地缓存能挡下大部分请求。
-
Caffeine.newBuilder().maximumSize(10000).expireAfterWrite(10, TimeUnit.MINUTES)是常见配置 - 本地缓存命中后不走 Redis,降低网络抖动影响
- 需注意多实例间本地缓存不一致问题,比如 A 实例更新了 DB 和 Redis,B 实例仍用旧本地副本
- 适合读多写少、容忍秒级不一致的场景,如用户基础信息、分类树
真正的难点不在代码,而在如何设计“本地缓存失效广播机制”——用 Redis Pub/Sub 或配置中心事件通知,都得小心循环触发和重复消费。
击穿问题从来不是选“哪个方案最好”,而是看你的数据变更多频繁、业务能接受多久不一致、团队有没有能力守住发布规范。布隆过滤器防不住击穿,空值缓存也防不住击穿,这些是穿透场景的解法,混用只会增加复杂度还解决不了问题。

















