缓存是高并发系统的第一道减压层,但 Redis 用不好,问题会被放大而不是被吸收。
线上最常见的三类故障:缓存穿透、缓存击穿、缓存雪崩。它们名字相似,但成因和治理策略完全不同。
本文给出一套可落地的防线设计:识别场景、匹配策略、验证效果。
【场景:一次促销把数据库打满】
典型链路是:
-
1. 大量请求命中同一热点商品。
-
2. Redis 某批 key 刚好过期。
-
3. 请求集中回源,数据库连接数飙升。
-
4. 上游超时重试,压力继续放大。
问题不在 Redis 本身,而在缓存策略没有按流量模型设计。
【问题1:缓存穿透(查不到的数据反复查)】
现象
-
• 请求参数非法或数据本身不存在。
-
• 每次都 miss,直接打数据库。
方案
-
1. 参数校验前置(ID 格式、范围、签名)
-
2. 空值缓存(短 TTL)
-
3. 布隆过滤器拦截不存在 key
String key = "product:" + productId; Object value = redis.get(key); if (value != null) return value; if (!bloomFilter.mightContain(productId)) { return null; } Object dbValue = db.query(productId); if (dbValue == null) { redis.setex(key, 60, "NULL"); return null; } redis.setex(key, 1800, dbValue); return dbValue;
【问题2:缓存击穿(热点 key 过期瞬间并发回源)】
现象
-
• 单个热点 key 失效。
-
• 大量并发同时回源同一条 SQL。
方案
-
1. 互斥锁重建缓存(单飞)
-
2. 热点 key 永不过期 + 后台异步刷新
-
3. 本地缓存兜底(短时间)
String lockKey = "lock:product:" + productId; if (redis.tryLock(lockKey, 10)) { try { Object dbValue = db.query(productId); redis.setex("product:" + productId, 1800, dbValue); } finally { redis.unlock(lockKey); } } else { Thread.sleep(30); return redis.get("product:" + productId); }
【问题3:缓存雪崩(大量 key 同时失效)】
现象
-
• 一批缓存在同一时刻过期。
-
• 数据库瞬时承压,出现连锁超时。
方案
-
1. TTL 加随机抖动(打散过期时间)
-
2. 多级缓存(本地 + Redis)
-
3. 限流降级(保护数据库)
int baseTtl = 1800; int jitter = ThreadLocalRandom.current().nextInt(0, 300); redis.setex(key, baseTtl + jitter, value);
【一套可复用的缓存防线模板】
public Object queryWithCache(Long id) {
if (!paramValid(id))
return null;
Object cached = redis.get(key(id));
if (cached != null) return cached;
if (!bloom.mightContain(id))
return null;
if (!rateLimiter.allow("cache-rebuild"))
return fallback();
return rebuildByMutexLock(id);
}
核心思路:
-
• 穿透:先拦截无效请求
-
• 击穿:控制重建并发
-
• 雪崩:打散失效时间 + 限流兜底
【缓存治理检查清单】
-
1. 是否区分了穿透、击穿、雪崩?
-
2. 是否配置了空值缓存与布隆过滤器?
-
3. 热点 key 是否有互斥重建策略?
-
4. TTL 是否做了随机抖动?
-
5. 是否有数据库保护措施(限流/降级)?
-
6. 是否有命中率与回源率监控?
下期预告:
《高并发保护实战:限流、熔断、降级如何配合落地》