一、Redis 缓存雪崩、击穿、穿透三大问题
雪崩是大面积 key 过期或节点故障,解决靠随机过期+集群高可用;
击穿是单个热点 key 失效,用互斥锁或逻辑过期;
穿透是查询不存在数据,用布隆过滤器+空值缓存拦截。
1.1、缓存雪崩(Cache Avalanche)
维度 | 要点 |
---|---|
定义 | 大量 key 同时过期 或 Redis 整体宕机,导致所有请求直达 DB。 |
触发场景 | • 批量 key 设置相同过期时间(如整点 00:00) • 节点故障、机房断网。 |
危害 | DB 瞬时 QPS 暴涨 → 连接池耗尽 → 服务整体不可用。 |
防御方案 | • 过期时间加随机值(打散)set key value EX $((base+rand)) • 永不过期 + 异步刷新(对热点 key) • 高可用集群(主从+哨兵 / Cluster) • 多级缓存(本地 Caffeine + Redis) • 熔断降级(Sentinel / Hystrix) |
1.2、缓存击穿(Cache Breakdown)
维度 | 要点 |
---|---|
定义 | 单个热点 key 在失效瞬间被高并发冲击,所有请求打到 DB。 |
触发场景 | • 热点 key 过期或被主动删除 • 微博热搜、秒杀商品。 |
危害 | 相比雪崩影响面小,但并发更高,可能打挂单节点 DB。 |
防御方案 | • 互斥锁(mutex)java<br>boolean lock = redisTemplate.opsForValue()<br> .setIfAbsent("lock:sku123", "1", 5, TimeUnit.SECONDS);<br>if (lock) { /* 查库并回写缓存 */ }<br> • 逻辑过期(value 里存过期时间,异步线程刷新) • 热点 key 永不过期 + 后台定时更新 |
1.3、缓存穿透(Cache Penetration)
维度 | 要点 |
---|---|
定义 | 大量请求访问根本不存在的数据 → Redis 无值 → DB 也无值,每次穿透。 |
触发场景 | • 恶意攻击随机 ID • 代码 bug 传入非法参数。 |
危害 | DB 空查也占连接,容易被刷爆。 |
防御方案 | • 布隆过滤器(预加载合法 ID) • 空对象缓存(short-term) setex not:sku999 "" 30s • 参数合法性校验(网关层 / 业务层) |
2、防护手段
实际项目中,我们在秒杀场景把三种手段叠加,最终把 DB QPS 控制在 200 以内。
- 本地 Caffeine 抗雪崩,
- Redis 互斥锁防击穿,
- 布隆过滤器挡穿透,
2.1、本地 Caffeine 抗雪崩(L1 缓存)
维度 | 说明 |
---|---|
问题定位 | 当 Redis 大面积失效或网络隔离,流量直接打到 DB,形成 缓存雪崩。 |
工作原理 | 在 每台应用实例 内再建一级 进程内缓存(Caffeine)。 • 读:先查 Caffeine → 未命中再查 Redis → 仍未命中才查 DB。 • 写:DB 更新后,先清 Redis,再 异步广播 清理本地缓存(基于 Redis Pub/Sub 或 Canal)。 |
关键配置 | java<br>Caffeine.newBuilder()<br> .maximumSize(10_000) // 最大条目<br> .expireAfterWrite(30, TimeUnit.SECONDS)<br> .recordStats() // 监控命中率<br> .build();<br> |
优势 | 1. 无网络 RTT,单机 QPS 可提升 1~2 个量级。 2. Redis 故障时仍能提供 有损服务,避免雪崩。 |
注意点 | • 内存占用:10 万条对象 ≈ 几十 MB。 • 一致性:本地 TTL 设短,或主动失效。 |
2.2、Redis 互斥锁 防击穿(L2 缓存)
维度 | 说明 |
---|---|
问题定位 | 单个热点 key 失效瞬间,大量并发击穿到 DB。 |
工作原理 | 只有一个线程能拿到锁,其余线程 阻塞或重试;拿到锁的线程去 DB 查询并回写缓存。 |
最简代码(Lua 原子锁) | java<br>// 1. 抢锁<br>Boolean locked = redisTemplate.execute(<br> RedisScript.of(<br> "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +<br> " redis.call('expire', KEYS[1], ARGV[2]); return 1; " +<br> "else return 0; end"),<br> Collections.singletonList("lock:sku123"),<br> UUID.randomUUID().toString(), "5");<br>// 2. 释放锁:用 Lua 保证“谁加的谁释放”<br> |
Redisson 高级方案 | • RLock lock = redisson.getLock("lock:sku123"); • 自带 看门狗(每 1/3 TTL 自动续期)防止业务执行时间 > TTL。 |
注意点 | • 锁超时必须 大于 99% 业务耗时。 • 高并发下 阻塞线程数 不能太多,可改用 逻辑过期(见下)。 |
2.3、布隆过滤器 挡穿透(L0 前置)
维度 | 说明 |
---|---|
问题定位 | 请求的是 根本不存在的数据(如 id = -1),Redis & DB 都无结果,导致每次穿透到 DB。 |
工作原理 | • 把所有 合法 key 预先通过 k 个 hash 函数映射到一个 位数组(bit array)。 • 查询时先走 BloomFilter: – 若返回 0 → 一定不存在,直接返回空。 – 若返回 1 → 可能存在(有极低误判率),继续查 Redis。 |
代码示例(Guava) | java<br>BloomFilter<String> bf = BloomFilter.create(<br> Funnels.stringFunnel(Charset.defaultCharset()),<br> 1_000_000, 0.01); // 100w 条,误判率 1%<br>// 初始化<br>productIds.forEach(id -> bf.put(id));<br>// 使用<br>if (!bf.mightContain(requestId)) {<br> return Result.empty();<br>}<br> |
Redis 模块 | • Redis 4.0+ 原生 RedisBloom 模块:BF.ADD / BF.EXISTS 。• 误判率可调: BF.RESERVE bf 0.001 500000 。 |
注意点 | • 只能 增查,不能 删(计数 BloomFilter 除外)。 • 误判率设太低 → 位数组过大,占用内存。 |
面试 30 秒话术:
我们线上用三级缓存三高:
• 布隆过滤器 在网关层先挡穿透;
• Caffeine 做 L1,30 s TTL,Redis 故障时兜底;
• Redis 互斥锁(Redisson)防击穿,看门狗续期,锁超时 5 s;
上线半年,缓存命中率 96%,DB QPS 从 3 w 降到 1 k。