缓存击穿的核心是多个线程重复加载同一数据,应优先用redis分布式锁(如set nx px)控制加载权,并配合双重检查与redisson可重入锁;加载逻辑需轻量、带超时与异常清理,空值缓存仅限明确不存在场景且ttl设2–5秒;本地+分布式缓存需同步清理或弃用本地缓存以保一致性。

缓存击穿发生时,get 方法返回 null 但多个线程同时触发加载
这是最典型的击穿表现:缓存过期瞬间,大量请求穿透到数据库,DB 压力陡增。关键不是“没缓存”,而是“多个线程重复加载同一份数据”。单纯加 synchronized 在方法上会锁住整个实例,吞吐暴跌;用 ConcurrentHashMap 的 computeIfAbsent 又只对本地缓存有效,跨 JVM 失效。
- 优先用分布式锁(如 Redis 的
SET key value NX PX 30000)控制加载权,而不是锁业务代码 -
computeIfAbsent可以用,但必须配合双重检查:先查本地缓存 → 查不到再抢分布式锁 → 抢到才查 DB 并写缓存 - 避免在锁内做耗时操作,比如远程调用、复杂计算;加载逻辑要轻量,失败需清理锁并重试机制
用 Redisson 实现可重入、自动续期的分布式锁
Redisson 的 RLock 能解决手动实现锁的续期、死锁、释放异常等问题。但默认不支持“锁等待超时”和“加载超时”的分离——你得自己控制加载任务的执行时限,否则一个慢查询可能让几十个线程卡在 lock.lock() 上。
- 用
tryLock(3, 10, TimeUnit.SECONDS):最多等 3 秒抢锁,持有锁最长 10 秒(自动看门狗续期) - 加载逻辑必须包裹在
try/finally中,确保unlock()执行,哪怕抛异常 - 不要把
RLock存为类成员变量,每次都要从RedissonClient获取新实例,否则锁状态会混乱
加载失败后,要不要设置空值缓存?
空值缓存(setex key 60 "null")能防穿透,但容易掩盖真实问题:比如 DB 连接池满、下游服务不可用。如果每次加载都失败,空值会把错误固化 60 秒,用户看到的全是“暂无数据”。
- 仅对明确的“数据不存在”(如查不到用户 ID)设空值,且 TTL 缩短至 2–5 秒
- 对异常失败(
SQLException、TimeoutException),不写空值,改用熔断或降级策略 - 监控
cache.null.hit指标,突增说明上游数据层出问题,不是缓存配置问题
本地缓存 + 分布式缓存组合时,Double-Check 容易漏掉更新场景
比如用 Caffeine 做本地缓存,Redis 做共享缓存。常见写法是:先查 Caffeine → 查不到再查 Redis → Redis 也没才加载。但更新时只清 Redis,本地缓存还留着脏数据,其他节点读不到最新值。
立即学习“Java免费学习笔记(深入)”;
- 更新操作必须同步清理所有节点的本地缓存,可用 Redis 的
PUBLISH通知或引入Cache2k的事件监听 - 或者干脆放弃本地缓存,用 Redis 的
LRU配合连接池优化,省去一致性维护成本 - 如果坚持用本地缓存,
refreshAfterWrite比expireAfterWrite更安全,它允许旧值继续服务,后台异步刷新
真正难的不是加锁或设空值,而是判断“这次加载到底该不该阻塞后续请求”——有些业务能接受短暂陈旧数据,有些必须强一致。方案得跟着读写比例、容忍延迟、故障恢复能力一起调,不是套个 RLock 就完事。










