一、概念解释
1. 缓存击穿(Cache Breakdown)
-
定义:某个
热点key在缓存过期(失效)的瞬间,大量请求同时访问这个数据,导致所有请求直接穿透到数据库 -
特点:针对单个热点key,在
过期瞬间并发访问量大 -
示例:热门商品详情页缓存过期时,大量用户同时点击
-
关键词: 热点key, 过期瞬间, 大量访问
-
总结: 热点key过期的瞬间, 这个时候再去从数据库读取, 然后再redis进行缓存, 已经来不及了, 因为是热点的key, 所以在那一瞬间, 高并发访问, 最终导致缓存击穿.
2. 缓存穿透(Cache Penetration)
- 定义:查询一个
数据库中根本不存在的数据,缓存无法命中,每次请求都直接访问数据库(数据库中没有, 那么redis中肯定也没有) - 特点:查询不存在的数据,可能是
恶意攻击或业务逻辑问题 - 示例:攻击者用不存在的用户ID频繁查询用户信息
- 关键词: 查询
压根就不存在的key
3. 缓存雪崩(Cache Avalanche)
- 定义:大量缓存数据在同一时间过期或Redis服务宕机,导致所有请求直接访问数据库
- 特点:影响范围广,多个key同时失效
- 示例:批量设置的缓存设置了相同的过期时间,到期时全部失效
二、严重程度比较
缓存雪崩 > 缓存击穿 > 缓存穿透
理由:
- 缓存雪崩:影响范围最广,可能直接导致数据库崩溃,系统整体不可用
- 缓存击穿:只影响单个热点key,但可能引发连锁反应
- 缓存穿透:通常只影响特定不存在的key,影响相对有限
三、解决方案
缓存击穿的解决方案
// 1. 使用互斥锁(分布式锁)
public Object getData(String key) {
Object data = redis.get(key);
if (data == null) {
// 获取分布式锁
if (lock.tryLock()) {
try {
// 双重检查
data = redis.get(key);
if (data == null) {
data = db.query(key); // 查询数据库
redis.setex(key, 3600, data); // 写入缓存
}
} finally {
lock.unlock();
}
} else {
// 等待其他线程加载数据
Thread.sleep(100);
return getData(key); // 重试
}
}
return data;
}
// 2. 设置热点数据永不过期 + 后台异步更新
redis.set(key, data); // 不设置过期时间
// 3. 逻辑过期时间
redis.set(key, {
"data": actualData,
"expire_time": System.currentTimeMillis() + 3600000 // 逻辑过期时间
});
缓存穿透的解决方案
// 1. 缓存空对象
public Object getData(String key) {
Object data = redis.get(key);
if (data != null) {
// 如果是空对象标记
if (EMPTY_OBJECT.equals(data)) {
return null;
}
return data;
}
data = db.query(key);
if (data == null) {
// 缓存空值,设置较短过期时间
redis.setex(key, 300, EMPTY_OBJECT);
return null;
}
redis.setex(key, 3600, data);
return data;
}
// 2. 布隆过滤器(推荐)
// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 预期数据量
0.01 // 误判率
);
// 查询前先检查
public Object getData(String key) {
if (!bloomFilter.mightContain(key)) {
return null; // 肯定不存在
}
// ... 正常查询流程
}
缓存雪崩的解决方案
// 1. 设置随机过期时间
public void setCache(String key, Object value) {
// 基础过期时间 + 随机时间(0-300秒)
int expireTime = 3600 + new Random().nextInt(300);
redis.setex(key, expireTime, value);
}
// 2. 缓存预热
// 系统启动时或定时任务预加载热点数据
public void cacheWarmUp() {
List<HotItem> hotItems = db.getHotItems();
for (HotItem item : hotItems) {
setCache("item:" + item.getId(), item);
}
}
// 3. 构建高可用Redis集群
// 使用哨兵模式或集群模式,避免单点故障
四、综合解决方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 互斥锁 | 缓存击穿 | 保证一致性,避免重复查询 | 降低吞吐量,实现复杂 |
| 布隆过滤器 | 缓存穿透 | 内存占用小,效率高 | 有误判率,不支持删除 |
| 缓存空对象 | 缓存穿透 | 实现简单 | 可能存储大量无效key |
| 随机过期时间 | 缓存雪崩 | 简单有效 | 不能完全避免雪崩 |
| 热点数据永不过期 | 缓存击穿/雪崩 | 完全避免过期问题 | 需要异步更新,可能数据不一致 |
| 多级缓存 | 所有场景 | 提供更好的可用性 | 架构复杂,一致性维护难 |
五、最佳实践建议
-
组合使用策略
- 热点数据:永不过期 + 异步更新
- 普通数据:随机过期时间 + 互斥锁
- 查询接口:布隆过滤器 + 参数校验
-
监控告警
// 监控缓存命中率 double hitRate = cacheHitCount / (cacheHitCount + cacheMissCount); if (hitRate < 0.8) { // 命中率低于80%告警 alert("缓存命中率过低"); } -
降级方案
// 数据库压力过大时降级 if (dbConnections > threshold) { // 返回默认数据或排队等待 return defaultData; } -
使用现代解决方案
- Redis 4.0+:使用LFU淘汰策略
- Redis 6.0+:使用客户端缓存
- 考虑使用Caffeine等本地缓存作为二级缓存
实际应用中,需要根据业务场景选择合适的组合方案,通常建议布隆过滤器防穿透 + 互斥锁防击穿 + 随机过期时间防雪崩的组合策略。