Redis 缓存击穿:热点 Key 过期打崩数据库的 3 种解决方案
缓存击穿、缓存穿透、缓存雪崩经常被混淆。本文用一个「首页热点配置」的真实故障,讲清楚三者的区别,重点给出缓存击穿的 3 种解决方案(互斥锁 / 逻辑过期 / 异步重建),并附可直接使用的代码。
一、问题案例
首页有一个「今日推荐」接口,查一个配置对象,QPS 数千。为了抗压,我给这个配置加了 Redis 缓存,过期时间 10 分钟。
上线第二天晚上,数据库连接数突然打满,首页接口大面积超时。监控显示:DB 的 QPS 从几百瞬间飙到几万,持续几秒又回落。
排查后发现,出问题的只有「今日推荐」这一个 key------它在过期的那一秒,几千个请求同时发现缓存没了,全部穿透到数据库。这不是雪崩,是击穿。
二、原理详解:击穿、穿透、雪崩的区别
| 类型 | 触发点 | 范围 | 典型场景 |
|---|---|---|---|
| 击穿 | 单个热点 key 过期瞬间被并发穿透 | 单个 key | 热门商品、首页配置 |
| 穿透 | 查询一个根本不存在的 key | 单个 key | 恶意请求、不存在的 id |
| 雪崩 | 大量 key 同时过期,或 Redis 宕机 | 全局 | 过期时间没打散、缓存层故障 |
击穿的关键特征:热点 key + 过期瞬间 + 高并发。三者缺一不可。普通 key 过期被穿透,压力不大;热点 key 但没并发,也不会打崩。
三、实战代码:3 种解决方案
方案一:互斥锁(最常用)
缓存未命中时,只让一个请求去查库并重建缓存,其余请求等待:
java
public String getConfig() {
String v = redis.get("config:today");
if (v != null) return v;
// 只有一个请求能拿到锁去查库重建
boolean locked = redis.setIfAbsent("config:today:lock", "1", 3, TimeUnit.SECONDS);
if (locked) {
try {
v = db.query();
redis.set("config:today", v, 10, TimeUnit.MINUTES);
} finally {
redis.delete("config:today:lock");
}
} else {
// 拿不到锁,稍等后重查一次缓存
Thread.sleep(50);
v = redis.get("config:today");
}
return v;
}
适用:数据强一致、允许短暂等待。缺点:拿锁失败的请求要重试,代码稍繁琐。
方案二:逻辑过期(不物理删除 key)
key 永不物理删除,值里存一个「逻辑过期时间戳」。读到已逻辑过期的值,先返回旧值顶着,后台异步重建:
java
class ConfigData {
String value;
long expireAt; // 逻辑过期时间戳
}
public String getConfig() {
ConfigData data = redis.get("config:today");
if (data == null) return rebuildWithLock();
if (System.currentTimeMillis() < data.expireAt) return data.value;
// 已逻辑过期:先返回旧值,异步重建
asyncRebuild();
return data.value;
}
适用:热点数据、对实时性不敏感。优点:用户永远不等待;缺点:会短暂返回旧值。
方案三:永不过期 + 后台定时刷新
热点 key 直接不设过期时间,用一个定时任务或消息队列,在数据变更时主动更新缓存。适合「数据量小、变更少、必须强一致」的配置类场景。
四、延伸:顺带防穿透
穿透(查不存在的 key)的防法:
- 空值缓存:把「不存在」也缓存起来(短过期时间),避免每次都打库。
- 布隆过滤器:在缓存前用布隆过滤器拦截不存在的 key。
五、总结
- 击穿、穿透、雪崩是三种不同的问题,先分清再下手:击穿是"一个热点 key 过期",穿透是"查不存在的 key",雪崩是"一大片同时失效或缓存挂了"。
- 击穿首选互斥锁(简单可靠),热点数据可用逻辑过期(不卡用户)。
- 顺手把空值缓存或布隆过滤器补上,挡住穿透。
我是无羡(小剑),全栈偏后端的独立开发者。
作品集:无羡 · 独立开发者作品集
如果对你有帮助,欢迎点赞、收藏、关注。