1. 缓存穿透(Cache Penetration)
问题描述
- 定义:大量请求查询数据库中不存在的数据,绕过缓存直接访问数据库。
- 原因:恶意攻击(如随机伪造 ID)或业务逻辑缺陷(如查询未校验的参数)。
- 影响:数据库压力骤增,可能引发宕机。
解决方案
方案 | 实现方式 | 优缺点 |
---|---|---|
布隆过滤器(Bloom Filter) | 在缓存层前加布隆过滤器,预先存储所有可能存在的 key,拦截非法请求。 | - 优点:高效拦截无效请求。 - 缺点:存在误判率,需定期更新过滤器数据。 |
缓存空值(Cache Null) | 对数据库中不存在的 key,缓存一个空值(如 NULL ),并设置较短的过期时间。 | - 优点:简单易实现。 - 缺点:可能缓存大量无效 key,占用内存空间。 |
请求参数校验 | 在业务层对请求参数进行合法性校验(如 ID 范围、格式)。 | - 优点:直接过滤非法请求。 - 缺点:无法完全覆盖所有非法参数场景。 |
2. 缓存击穿(Cache Breakdown)
问题描述
- 定义:某个热点 key 突然过期,大量并发请求直接击穿缓存,访问数据库。
- 原因:高并发场景下,热点 key 过期后重建缓存耗时较长。
- 影响:数据库瞬时压力极大,可能导致服务雪崩。
解决方案
方案 | 实现方式 | 优缺点 |
---|---|---|
互斥锁(Mutex Lock) | 使用分布式锁(如 Redis 的 SETNX ),保证同一时间只有一个线程重建缓存。 | - 优点:避免并发重建。 - 缺点:锁竞争可能增加延迟,需处理锁超时问题。 |
逻辑过期(Logical Expire) | 缓存数据不设置物理过期时间,而是存储逻辑过期时间,由后台异步更新缓存。 | - 优点:缓存永不过期,避免击穿。 - 缺点:需维护异步更新逻辑,复杂性高。 |
热点数据永不过期 | 对极热点 key 设置为永不过期,通过定时任务或事件驱动更新缓存。 | - 优点:彻底避免击穿。 - 缺点:数据更新延迟,需额外维护更新逻辑。 |
3. 缓存雪崩(Cache Avalanche)
问题描述
- 定义:大量缓存 key 同时过期 或 缓存服务宕机,导致请求直接访问数据库。
- 原因:缓存 key 过期时间集中,或 Redis 集群故障。
- 影响:数据库压力过大,可能引发级联故障。
解决方案
方案 | 实现方式 | 优缺点 |
---|---|---|
随机过期时间 | 为每个 key 的过期时间添加随机值(如基础时间 + 随机分钟),避免集中过期。 | - 优点:简单有效。 - 缺点:无法完全避免部分 key 同时过期。 |
多级缓存架构 | 结合本地缓存(如 Caffeine)和分布式缓存(如 Redis),分散缓存失效风险。 | - 优点:提高系统容错性。 - 缺点:架构复杂性增加,数据一致性需处理。 |
熔断降级机制 | 使用熔断器(如 Hystrix)或限流工具(如 Sentinel),在缓存失效时降级处理请求。 | - 优点:保护数据库不被压垮。 - 缺点:可能影响用户体验。 |
集群高可用 | 部署 Redis 集群(如哨兵模式、Cluster 模式),确保缓存服务的高可用性。 | - 优点:避免单点故障。 - 缺点:运维成本增加。 |
4. 综合方案与最佳实践
防御组合
- 穿透:布隆过滤器 + 缓存空值 + 参数校验。
- 击穿:互斥锁 + 逻辑过期 + 热点数据预热。
- 雪崩:随机过期时间 + 多级缓存 + 熔断降级。
代码示例一(Redis 实现互斥锁)
public String getData(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key + ":lock", "1", 10)) { // 获取分布式锁
try {
value = db.query(key); // 查询数据库
redis.setex(key, 3600, value); // 写入缓存
} finally {
redis.del(key + ":lock"); // 释放锁
}
} else {
Thread.sleep(100); // 等待后重试
return getData(key);
}
}
return value;
}
代码示例二(Redisson 实现互斥锁)
public String getData(String key) {
RLock lock = redissonClient.getLock(LOCK_KEY);
lock.lock();
try {
······
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return value;
}