Redis 缓存穿透、击穿和雪崩是高并发场景下的典型问题,以下是详细解决方案和最佳实践:
一、缓存穿透(Cache Penetration)
问题:恶意请求不存在的数据(如不存在的ID),绕过缓存直接访问数据库。
解决方案:
-
布隆过滤器(Bloom Filter)
- 前置过滤器拦截非法请求,存储所有合法键的指纹。
- 实现示例:
# 使用RedisBloom模块(需安装) # 创建布隆过滤器,预期容量100万,错误率1% BF.RESERVE my_filter 0.01 1000000 # 将合法ID加入过滤器 BF.ADD my_filter valid_id_1 # 查询前先检查过滤器 if BF.EXISTS my_filter request_id: # 查缓存或数据库 else: return None
-
缓存空对象(Null Caching)
- 对查询结果为空的请求,缓存短时效的空值。
- 实现示例:
Object value = redis.get(key); if (value == null) { value = db.query(key); if (value == null) { // 缓存空值,设置较短过期时间(如30秒) redis.setex(key, 30, "NULL"); } else { redis.setex(key, 3600, value); } } else if ("NULL".equals(value)) { return null; // 避免数据库查询 }
最佳实践:
- 结合布隆过滤器与空对象缓存:先用过滤器拦截绝对不存在的数据,对可能存在的Key缓存空值。
- 定期清理空值缓存,避免内存浪费。
二、缓存击穿(Cache Breakdown)
问题:热点Key突然过期,大量并发请求直接击穿到数据库。
解决方案:
-
互斥锁(Mutex Lock)
- 使用分布式锁控制单线程重建缓存。
- 实现示例(Redis分布式锁):
def get_data(key): data = redis.get(key) if data is None: # 尝试获取锁(SETNX + 超时时间) if redis.setnx("lock:" + key, 1, ex=10): try: data = db.query(key) redis.setex(key, 3600, data) finally: redis.delete("lock:" + key) else: # 等待并重试或返回默认值 time.sleep(0.1) return get_data(key) return data
-
逻辑过期(Logical Expiration)
- 缓存永不过期,但存储逻辑过期时间,异步更新。
- 数据结构示例:
{ "value": "真实数据", "expire": 1715000000 // 逻辑过期时间戳 }
- 后台线程检测过期时间并主动更新。
最佳实践:
- 对热点Key启用永不过期策略,结合异步更新(如定时任务或消息队列触发更新)。
- 使用Redisson等成熟库实现分布式锁,避免手写锁逻辑出错。
三、缓存雪崩(Cache Avalanche)
问题:大量缓存Key同时过期,导致数据库负载骤增。
解决方案:
-
随机过期时间
- 基础过期时间 + 随机偏移量(如30分钟±随机600秒)。
- 示例代码:
int baseExpire = 1800; // 30分钟 int randomExpire = ThreadLocalRandom.current().nextInt(-600, 600); redis.setex(key, baseExpire + randomExpire, value);
-
多级缓存架构
- 本地缓存(如Caffeine) + Redis + 数据库。
- 流程:
- 先查本地缓存
- 本地未命中则查Redis
- Redis未命中则查数据库并回填
-
服务熔断与降级
- 使用Hystrix或Sentinel在数据库压力过大时触发降级策略(如返回默认值)。
最佳实践:
- 关键数据设置分层过期时间(如核心数据永不过期+异步刷新)。
- 启用Redis Cluster或Sentinel实现高可用,避免单点故障。
综合防御策略
- 监控与预热:
- 实时监控缓存命中率,热点数据提前预热。
- 使用Redis的
SLOWLOG
分析慢查询。
- 压测验证:
- 模拟极端场景(如缓存宕机),验证降级策略是否生效。
- 代码示例(综合方案):
public Object getData(String key) { // 1. 布隆过滤器拦截 if (!bloomFilter.mightContain(key)) { return null; } // 2. 查缓存 Object value = redis.get(key); if (value != null) { return "NULL".equals(value) ? null : value; } // 3. 获取分布式锁重建缓存 RLock lock = redisson.getLock("lock:" + key); try { lock.lock(); // 二次检查缓存 value = redis.get(key); if (value == null) { value = db.query(key); redis.setex(key, 300 + random.nextInt(60), value == null ? "NULL" : value); } return value; } finally { lock.unlock(); } }
总结
- 穿透:布隆过滤器 + 空值缓存。
- 击穿:分布式锁 + 逻辑过期。
- 雪崩:随机TTL + 多级缓存。
- 所有方案需结合业务场景调整参数(如过期时间、锁超时时间),并通过压测验证可靠性。