上周商城搞活动,一款热门商品的详情页在开抢前 5 分钟突然变慢,数据库 CPU 从 20% 飙到 95%。我查 Redis 发现那个商品的缓存 key 刚好过期了,一瞬间几千个请求同时穿过缓存打到数据库,把连接池占满。
这种就是典型的缓存击穿——某个热点 key 失效瞬间,大量并发请求直接打数据库。前后花了两天把防护加上,记录下来。
一、先分清三个概念
缓存问题容易混,先理清楚:
- 缓存穿透:查一个根本不存在的数据,缓存里没有,每次都打数据库(攻击者恶意查不存在的 ID)。
- 缓存击穿:某个热点 key 过期瞬间,大量并发请求同时打数据库。
- 缓存雪崩:大量 key 同时过期,整个缓存层失效,全打数据库。
这次是缓存击穿:单个热点商品的 key 过期了。
二、互斥锁:只让一个请求去重建缓存
热点 key 过期时,只放一个请求去查数据库重建缓存,其他请求等一下再读缓存:
// 伪代码
public Product getProduct(Long id) {
String key = "product:" + id;
Product p = redis.get(key);
if (p != null) return p;
// 缓存miss,抢锁
String lockKey = "lock:product:" + id;
try {
// SET NX EX 10,只放一个请求成功
Boolean locked = redis.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
p = db.queryProduct(id);
redis.setex(key, 3600, p);
return p;
} else {
// 没抢到锁,等200ms再读缓存
Thread.sleep(200);
return getProduct(id);
}
} finally {
redis.del(lockKey);
}
}
关键参数:锁超时 10 秒(防止拿到锁的请求挂了死锁),没抢到锁的等 200ms 重试,别无限递归。
三、热点 key 永不过期:逻辑过期
对确定会很热的商品,缓存不设 TTL,改成逻辑过期——在 value 里存一个过期时间,后台异步更新:
// value 里存逻辑过期时间
{
"product": {...},
"expireAt": 1728000000000 // 逻辑过期时间
}
// 读的时候判断逻辑过期,异步更新
public Product getProductLogic(Long id) {
String raw = redis.get("product:" + id);
CacheData data = parse(raw);
if (data.getExpireAt() < System.currentTimeMillis()) {
// 逻辑过期,开异步线程更新缓存
asyncRefresh(id);
}
return data.getProduct(); // 返回旧数据,用户无感知
}
这样缓存永远不会物理过期,不存在"击穿瞬间"。代价是用户可能读到几秒旧数据,商品详情页可以接受。
四、布隆过滤器:挡住查不存在 ID 的请求
缓存穿透是另一个问题——攻击者查不存在的商品 ID,缓存永远 miss。用布隆过滤器在缓存前挡一道:
// 启动时把所有商品ID加载到布隆过滤器
BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.01);
for (Long id : allProductIds) {
filter.put(id);
}
// 请求进来先过布隆过滤器
public Product getProduct(Long id) {
if (!filter.mightContain(id)) {
return Product.notFound(); // 一定不存在,直接返回
}
// ... 正常查缓存
}
误判率设 1%,布隆过滤器占内存约 1.2MB(100 万条)。不存在的请求在这一层就被挡住,不会打到数据库。
五、踩坑清单(5 条实际踩过的)
- 互斥锁必须设超时(EX 10s),否则拿到锁的请求挂了会死锁,所有请求都卡在等锁。
- 没抢到锁的请求别同步等待太久,sleep 200ms 重试,递归次数要有上限。
- 热点 key 永不过期要配后台异步刷新,否则缓存和数据库最终会不一致。
- 布隆过滤器不支持删除,商品下架时要重建过滤器,别直接删 key。
- 大量 key 同时过期要加随机抖动:expire = 3600 + random(0, 300),避免雪崩。
| 问题类型 | 防护手段 | 适用场景 |
|---|---|---|
| 缓存击穿 | 互斥锁/逻辑过期 | 单个热点 key |
| 缓存穿透 | 布隆过滤器/空值缓存 | 查不存在数据 |
| 缓存雪崩 | TTL 随机抖动 | 大量 key 同时过期 |
这次商品详情页的缓存防护是在乔拓云的商城系统上做的,我主要负责加热点 key 互斥锁、配逻辑过期和接布隆过滤器那几段,改完后活动期间数据库 CPU 稳定在 30% 以下,再没出现打挂的情况。
复盘
- 缓存击穿是热点 key 过期瞬间并发打 DB,用互斥锁或逻辑过期解决。
- 布隆过滤器挡穿透,TTL 随机抖动防雪崩,三个问题三种手段。
- 互斥锁必须设超时,逻辑过期要配异步刷新,否则会引入新问题。
以上是个人实践记录,各平台具体功能以官方实时信息为准。
你们做缓存防护时,热点 key 是用互斥锁还是逻辑过期?