双缓存不能杜绝缓存击穿,真正起效的是锁+逻辑过期+主动reload组合策略;本地缓存miss后须二次检查并加分布式锁,redis层禁用物理过期而采用逻辑过期,本地缓存需主动reload而非依赖ttl。

双缓存本身不能“彻底杜绝”缓存击穿——它只是把击穿风险从数据库前移到了本地缓存前,真正起作用的是锁 + 逻辑过期 + 主动 reload 的组合策略。
为什么双缓存(本地 + Redis)不是防击穿的银弹
常见误解是:加一层本地缓存(如 Caffeine)就能挡住 Redis 失效时的并发穿透。但现实是:localCache.get(key) 返回 null 后,多个线程会同时去查 redisTemplate.opsForValue().get(key);如果 Redis 里也刚好过期或为空,它们又会一起涌向 DB。
本地缓存只靠 TTL 被动失效,无法解决“失效窗口内高并发重叠”问题。必须配合显式同步控制。
本地缓存层必须加二次检查 + 分布式锁
本地缓存 miss 后,不能直接查 Redis 或 DB,要走标准互斥流程:
- 先尝试用
redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS)获取分布式锁 - 没拿到锁,就 sleep(50ms) 后重试,或直接返回兜底值(如空集合、默认对象)
- 拿到锁后,必须再次调用
localCache.get(key)和redisTemplate.opsForValue().get(key)——防止其他线程已写入 - 只有两者都为空,才查 DB;查完后,先写 Redis(带逻辑过期字段),再写本地缓存(可设无 TTL 或长 TTL)
Redis 层必须用逻辑过期,禁用物理 EXPIRE
物理过期(EXPIRE key 300)会让 key 瞬间消失,触发击穿。逻辑过期是把时间戳嵌在 value 里:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
{"data": {"id": 1001, "name": "iPhone 16"}, "expireAt": 1725416820000}
读取时反序列化后判断 expireAt ;过期则尝试加锁重建,抢锁失败就返回旧数据(允许 stale read),不阻塞请求。
写入时不要调用 redisTemplate.expire(),key 永不过期,仅靠业务逻辑判断时效性。
本地缓存 reload 不是可选项,而是必做动作
很多团队只配置了 Caffeine.newBuilder().maximumSize(10000).expireAfterWrite(5, TimeUnit.MINUTES) 就以为万事大吉,这是最大隐患。
高频热点 key 必须主动刷新:
- DB 写成功后,立刻调用
localCache.put(key, newValue),而不是等自然过期 - 对极热 key(如首页 banner、秒杀商品),启动后台线程每 90 秒调用一次
reload(key),避免集中失效 -
maximumSize必须设硬上限,否则本地缓存 OOM 风险远高于 Redis
多级缓存最难的从来不是“怎么搭两层”,而是三端(本地缓存 / Redis / DB)状态不同步时,哪一环该以谁为准、何时该跳过、何时该阻塞——这些边界条件漏掉一个,击穿就会在某个毛刺时刻复现。










