Redis缓存雪崩并不是一个独立的Redis功能问题,而是一种缓存层异常导致后端数据库承压的现象。简单来说,当缓存中大量key在同一时间过期失效,或者Redis实例因为故障重启后没有数据,所有读取请求都会绕过缓存直接落到数据库。数据库的并发能力和连接池资源远比Redis有限,一旦请求量超过阈值,就会出现大量慢查询、连接占满甚至服务不可用。因此,预防缓存雪崩的重点不是阻止缓存过期,而是让过期过程变得平滑,让后端始终有足够保护。

一、缓存雪崩的成因与触发条件
缓存雪崩最常见的诱因是批量设置缓存时使用了相同的过期时间。比如一个商品列表接口会缓存最近上架的1000个商品,如果每个key都设置了600秒过期,那么在写入缓存后的第600秒,这1000个key会集中失效。此时如果有用户访问这些商品,缓存无法命中,请求会直接查询数据库,造成瞬时压力。类似场景还有活动开始前统一预热的秒杀库存、定时任务批量刷新的配置项等。
另一种成因是Redis服务自身不可用。无论是服务器宕机、网络分区还是内存达到上限触发淘汰,都可能让大量缓存数据在短时间内消失。如果配置了maxmemory-policy allkeys-lru,Redis会在内存不足时优先淘汰最近较少使用的key,当淘汰速度过快时,缓存命中率会显著下降,表现和雪崩非常相似。此时单看数据库QPS曲线,会看到一条陡峭的上升线。
需要把缓存雪崩和缓存穿透、缓存击穿区分开。缓存穿透是请求根本不存在的key,缓存和数据库中都没有;缓存击穿是指某个热点key在过期的瞬间被超高并发请求直接打到数据库;而缓存雪崩则更偏向于大量key同时失效或者缓存服务整体不可用。虽然三者的防护思路有重合,但雪崩更强调过期时间分布、缓存集群容灾和整体容量保护。
二、给过期时间加随机值,并引入多级缓存
为了避免大量key在同一时刻失效,最直接的做法是在设置TTL时加入随机值。业务上可能要求缓存10分钟,但不必每个key都精确600秒,可以设置600秒加上一个0到120秒之间的随机偏移。这样缓存过期时间被分散到10到12分钟之间,同一批写入的key不会在同一秒全部过期。下面是一段使用Jedis设置随机过期时间的示例:
Random random = new Random(); String key = "product:1001"; String value = "detail"; int baseTtl = 600; int offset = random.nextInt(120); jedis.setex(key, baseTtl + offset, value);
随机偏移的范围需要结合业务容忍度来定。偏移太小起不到分散作用,偏移太大又会让缓存保存时间超出业务预期,可能造成数据更新延迟。通常建议在基础TTL的5%到15%之间随机,例如300秒基础时间加15到45秒偏移。如果TTL本身很长,比如24小时,可以放大到1到3小时的随机区间。对于缓存内容变化不敏感的数据,随机范围可以更宽。
热点key即使加了随机TTL,仍然可能在过期瞬间被大量请求集中访问。针对这种情况可以使用逻辑过期,不真正删除key,而是把过期时间记录在value内部。读取时判断逻辑时间是否过期,如果过期则先返回旧值,同时异步去数据库加载新值,避免请求全部阻塞在回源过程。逻辑过期示例如下:
class CacheObject {
private String data;
private long expireAt;
public boolean isExpired() {
return System.currentTimeMillis() > expireAt;
}
}
CacheObject obj = getFromRedis(key);
if (obj == null || obj.isExpired()) {
// 先返回旧数据,再异步刷新
}
多级缓存也能缓解雪崩。应用本地可以使用Caffeine或Guava Cache作为一级缓存,Redis作为二级缓存,数据库作为最终数据源。当Redis中的某个key失效时,本地缓存可能仍然有数据,请求不会立刻穿透到数据库。本地缓存设置几秒到几十秒的短过期时间,让Redis重建期间有缓冲。需要注意的是,多级缓存会带来一致性问题,建议在写操作时主动清理本地缓存和Redis,或者通过消息通知各节点更新。
三、Redis高可用架构与容灾
单纯调整过期时间无法解决Redis整个实例宕机导致的雪崩。如果只有一个Redis节点,任何硬件故障、重启或网络中断都会让所有缓存瞬间失效。因此需要在部署架构上消除单点风险。常见做法是使用主从复制配合哨兵模式,主节点负责写入,从节点承担读请求,主节点故障时哨兵自动提升从节点为主节点。对于数据量特别大的场景,可以采用Redis Cluster分片,将key分散到不同节点,单个节点故障只影响一部分数据,不会出现全量雪崩。
无论使用哨兵还是集群,持久化配置都不可忽略。如果没有持久化,Redis重启后缓存全空,请求会在一开始就涌入数据库。建议同时开启RDB和AOF,并以混合持久化方式运行。这样既能保证恢复速度快,又能减少数据丢失。核心配置示例如下:
# Redis持久化配置 save 900 1 save 300 10 save 60 10000 appendonly yes appendfsync everysec aof-use-rdb-preamble yes
主从切换虽然可以自动完成,但切换期间仍然可能出现短暂不可写入的问题。应用端需要做好重试和超时控制,避免在故障转移阶段大量请求超时后直接打到数据库。可以在Redis客户端配置合理的连接超时和重试策略,例如使用Lettuce的拓扑刷新机制,让客户端及时感知节点变化。对于核心服务,还可以部署多套Redis实例,按业务域拆分缓存,避免所有业务依赖同一套缓存。
四、限流降级、互斥锁与缓存预热
当缓存雪崩已经发生时,最重要的是保护数据库不被打垮。可以在缓存重建阶段使用互斥锁,只允许一个请求去数据库查询并写入缓存,其余请求等待锁释放后直接读缓存。这样可以避免成百上千个请求同时查询数据库。使用Redis实现分布式锁示例如下:
String lockKey = "lock:product:1001";
String requestId = UUID.randomUUID().toString();
boolean locked = "OK".equals(jedis.set(lockKey, requestId, "NX", "EX", 30));
if (locked) {
try {
// 查询数据库并写入缓存
} finally {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
}
除了互斥锁,还需要在应用层做限流和降级。可以为数据库访问单独设置线程池,避免缓存失效的请求占用所有业务线程。当发现缓存命中率骤降或数据库压力过大时,对非核心接口返回兜底数据或默认值,例如商品详情页返回缓存的静态快照、推荐位返回预设列表。限流可以采用令牌桶或漏桶算法,在网关层对数据库相关接口限制QPS,超过阈值的请求直接降级。
缓存预热是预防雪崩的重要补充,尤其在系统重启或大促前。如果Redis刚刚启动、缓存为空,此时大量用户请求会直接穿透到数据库,形成冷启动雪崩。可以在上线前执行预热任务,扫描数据库中的热点数据,提前写入Redis并设置合理的过期时间。预热可以是定时任务,也可以由运营后台触发。预热完成后,再逐步放开入口流量,让缓存先承接大部分请求。预热数据的TTL同样要加随机值,避免预热批次本身在后续某一时刻集中失效。
综合来看,预防Redis缓存雪崩需要从多个层面共同配合。过期时间随机化解决同时失效问题,逻辑过期和互斥锁解决热点key回源压力,主从哨兵或集群架构解决缓存服务可用性问题,而限流降级和缓存预热则是在极端情况下保护数据库的最后屏障。实际落地时不需要一次性启用所有策略,可以先根据业务流量和缓存规模评估风险,优先改进过期策略和缓存部署架构,再逐步加入多级缓存与预热机制。