Redis,什么是缓存穿透/击穿/雪崩,如何解决它们
基于 Redis 7.0 的 图解 + 实战代码 完整方案
让你 10 分钟 搞懂 三大缓存杀手 及其 克星!
目录
- 导语:三大杀手初印象
- [缓存穿透(Cache Penetration)](#缓存穿透(Cache Penetration) "#%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F")
- [缓存击穿(Cache Breakdown)](#缓存击穿(Cache Breakdown) "#%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF")
- [缓存雪崩(Cache Avalanche)](#缓存雪崩(Cache Avalanche) "#%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9")
- 对比表:一眼看懂区别
- 实战:Spring Boot + Redis 代码实现
- 压测与调优建议
- 总结:口诀记忆
导语:三大杀手初印象
场景 | 描述 | 危害 |
---|---|---|
穿透 | 查询 不存在 的数据,缓存 不命中,DB 被拖垮 | QPS 暴涨,DB 宕机 |
击穿 | 热点 突然失效 ,大量并发 打到 DB | DB 连接耗尽 |
雪崩 | 大量 key 同时过期 或 Redis 宕机 | 整个系统崩溃 |
缓存穿透(Cache Penetration)
1. 流程图
sequenceDiagram
participant App
participant Redis
participant DB
App->>Redis: get user:9999
Redis->>App: nil
App->>DB: select * from user where id=9999
DB->>App: null
App->>Redis: set user:9999 null
2. 解决方案
方案 | 说明 | 代码片段 |
---|---|---|
布隆过滤器 | 拦截 不存在 的 key | BloomFilter.put("user:9999") |
空值缓存 | 缓存 NULL 并设置 短过期时间 | redis.setex("user:9999", 30, "NULL") |
3. 布隆过滤器(RedisBloom)
java
// 初始化
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user-filter");
bloomFilter.tryInit(1000000, 0.03);
// 放入
bloomFilter.add("user:1");
// 判断
if (!bloomFilter.contains("user:9999")) {
return null; // 直接返回,不查 DB
}
缓存击穿(Cache Breakdown)
1. 场景
- 热点数据 (如秒杀商品)过期瞬间 → 万级并发 打到 DB。
2. 解决方案
方案 | 说明 | 代码片段 |
---|---|---|
互斥锁(分布式锁) | 仅 一个线程 查 DB,其余等待 | RLock lock = redisson.getLock("lock:item:1") |
逻辑过期(永不过期) | 把 过期时间放在 value 里,后台异步刷新 | value = {data:xxx, expire:1719999999} |
3. 分布式锁实现
java
public Item getItem(Long id) {
String key = "item:" + id;
Item item = redis.get(key);
if (item != null) {
return item; // 命中缓存
}
// 未命中,加锁
RLock lock = redisson.getLock("lock:item:" + id);
try {
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
item = redis.get(key); // 双重检查
if (item != null) return item;
item = db.findById(id);
redis.setex(key, 300, item); // 5 分钟过期
return item;
}
// 等待后重试
Thread.sleep(50);
return getItem(id);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
缓存雪崩(Cache Avalanche)
1. 场景
- 大量 key 同时过期 (如凌晨全量失效)→ DB 被打爆。
- Redis 宕机 → 所有流量打到 DB。
2. 解决方案
方案 | 说明 | 代码片段 |
---|---|---|
随机过期时间 | 打散失效时间 | expire = base + random(0, 600) |
永不过期 + 后台刷新 | 逻辑过期,异步更新 | value = {data:xxx, expire:xxx} |
Redis 高可用 | 主从 + 哨兵 / Cluster | spring.redis.cluster.nodes=node1:6379,node2:6379 |
3. 随机过期时间实现
java
public void setWithRandomExpire(String key, Object value) {
int base = 3600; // 1小时
int random = new Random().nextInt(600); // 0-600秒
redis.setex(key, base + random, value);
}
对比表:一眼看懂区别
问题 | 触发条件 | 现象 | 解决方案 |
---|---|---|---|
穿透 | 查询 不存在 数据 | DB 被 不存在 查询打爆 | 布隆过滤器 或 空值缓存 |
击穿 | 热点 突然失效 | DB 被 同一 key 打爆 | 分布式锁 或 逻辑过期 |
雪崩 | 大量 key 同时失效 | DB 被 所有 key 打爆 | 随机过期 或 永不过期 |
实战:Spring Boot + Redis 代码实现
1. 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version>
</dependency>
2. 配置类
java
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
3. 统一缓存模板(防穿透 + 防击穿)
java
@Service
public class CacheService {
@Autowired
private RedissonClient redisson;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public <T> T get(String key, Supplier<T> dbFallback, Class<T> clazz) {
T value = (T) redisTemplate.opsForValue().get(key);
if (value != null) return value;
// 布隆过滤器防穿透
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("bloom");
bloomFilter.tryInit(1000000, 0.03);
if (!bloomFilter.contains(key)) {
return null;
}
// 分布式锁防击穿
RLock lock = redisson.getLock("lock:" + key);
try {
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
value = (T) redisTemplate.opsForValue().get(key);
if (value != null) return value;
value = dbFallback.get();
if (value == null) {
redisTemplate.opsForValue().set(key, "NULL", 30, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(key, value, 3600 + new Random().nextInt(600), TimeUnit.SECONDS);
}
return value;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
return null;
}
}
压测与调优建议
参数 | 建议值 | 说明 |
---|---|---|
batch.size | 32 KB | 提高吞吐 |
linger.ms | 5-20 ms | 延迟换吞吐 |
compression.type | lz4 | 压缩比 + CPU 平衡 |
fetch.min.bytes | 1 KB | 减少空拉取 |
num.network.threads | CPU 核数 | 网络 I/O 线程 |
总结:口诀记忆
穿透 → 布隆拦
击穿 → 锁来扛
雪崩 → 随机散
Redis 宕机 → 高可用上
掌握 "布隆锁 + 随机散 + 高可用" 三件套,
你就拥有了 抗住百万并发 的 终极缓存方案!