2. 缓存基础:缓存穿透、缓存击穿、缓存雪崩的定义与经典解决方案
在分布式缓存系统中,缓存穿透、缓存击穿和缓存雪崩是三种最典型的高频故障场景。它们虽然名称相似,但成因、影响范围和治理策略截然不同。本节将逐一拆解其定义,并给出经过生产验证的经典解决方案,为后续基于 OpenAPI 契约的失效策略治理奠定基础。
2.1 三大故障的定义与对比
下表从触发条件、影响范围和典型表现三个维度,对三种故障进行横向对比:
| 故障类型 | 触发条件 | 影响范围 | 典型表现 |
|---|---|---|---|
| 缓存穿透 | 查询一个不存在的数据(key 在缓存和数据库中均不存在) | 所有不存在 key 的请求直接打到数据库 | 数据库 QPS 突增,缓存命中率骤降 |
| 缓存击穿 | 一个热点 key 在缓存失效的瞬间,大量并发请求同时回源 | 单个热点 key 失效导致数据库瞬时压力 | 数据库单行记录被高频并发查询 |
| 缓存雪崩 | 大量 key 在同一时间窗口内批量失效,或缓存节点宕机 | 大面积缓存失效,数据库被海量请求淹没 | 数据库整体 QPS 飙升,服务级联宕机 |
核心认知:穿透是“查不到”,击穿是“热点失效”,雪崩是“批量失效”。三种故障的治理策略分别对应:空值兜底、互斥回源、失效分散。
2.2 缓存穿透:空值兜底与布隆过滤器
2.2.1 问题定义
当客户端持续请求一个数据库中也不存在的数据(如查询一个已被删除的用户 ID),缓存层永远无法命中,所有请求都会穿透缓存直达数据库。攻击者常利用此特性发起“穿透攻击”,导致数据库连接池耗尽。
2.2.2 经典解决方案
- 方案一:缓存空值(Null Object Pattern)
- 将查询结果为 null 的 key 也写入缓存,设置较短的 TTL(如 30~60 秒)。
- 优点:实现简单,对现有代码侵入小。
- 缺点:需要额外的存储空间存储空 key,且无法完全防御恶意构造的不同 key。
- 方案二:布隆过滤器(Bloom Filter)
- 在缓存层之前增加布隆过滤器,用于快速判断 key 是否“可能存在”。
- 如果布隆过滤器判定 key 不存在,则直接拒绝请求,不再查询缓存和数据库。
- 优点:空间效率极高,能拦截绝大多数不存在 key 的请求。
- 缺点:存在一定的误判率(可能把不存在判为存在),且无法删除元素(需定期重建)。
生产建议:对于用户 ID、订单号等有明确范围的 key,优先使用布隆过滤器;对于查询条件不可枚举的场景,采用“缓存空值 + 短 TTL”作为兜底。
2.3 缓存击穿:互斥锁与逻辑过期
2.3.1 问题定义
某个被高频访问的“热点 key”(如秒杀商品库存、首页 Banner 配置)在缓存失效的瞬间,大量并发请求同时发现缓存未命中,全部回源查询数据库。数据库在极短时间内承受数倍于平时的并发压力,可能导致单行记录锁竞争或连接超时。
2.3.2 经典解决方案
- 方案一:互斥锁(Mutex Lock)
- 当缓存未命中时,先尝试获取分布式锁(如 Redis SETNX)。
- 只有成功获取锁的线程才去查询数据库并重建缓存;其他线程等待或重试。
- 优点:保证只有一个请求回源,数据库压力可控。
- 缺点:引入锁等待,可能增加响应延迟;需处理锁超时和死锁问题。
- 方案二:逻辑过期(Logical Expiration)
- 缓存数据永不过期(物理 TTL 很大),但在数据中嵌入一个逻辑过期时间字段。
- 当读取到逻辑过期时,当前线程立即返回旧数据,同时异步启动一个后台线程去更新缓存。
- 优点:无锁、无等待,响应速度极快。
- 缺点:数据存在短暂的不一致窗口;需要额外的异步更新机制。
避坑提示:互斥锁方案中,锁的粒度必须精确到 key 级别,切勿使用全局锁;逻辑过期方案中,异步更新线程必须做去重控制,防止重复回源。
2.4 缓存雪崩:失效分散与高可用
2.4.1 问题定义
缓存雪崩有两种典型诱因:一是大量 key 设置了相同的过期时间(如统一在凌晨 0 点失效),导致缓存同时大面积失效;二是缓存节点宕机或网络分区,导致整个缓存层不可用。两种情况下,所有请求都会瞬间涌向数据库,极易引发数据库宕机和服务雪崩。
2.4.2 经典解决方案
- 方案一:失效时间随机化
- 在设置缓存 TTL 时,在基础过期时间上增加一个随机偏移量(如 ±10%~30%)。
- 避免大量 key 在同一时刻集中失效。
- 方案二:多级缓存架构
- 引入本地缓存(如 Caffeine、Guava Cache)作为 L1 缓存,Redis 作为 L2 缓存。
- 即使 Redis 宕机,本地缓存仍能承载部分流量,为数据库争取恢复时间。
- 方案三:缓存高可用与限流熔断
- Redis 采用主从 + Sentinel 或 Cluster 模式,避免单点故障。
- 在数据库访问层配置限流(如 Sentinel、Hystrix),当数据库压力超过阈值时直接熔断,返回降级数据。
2.5 三种故障治理策略总览
下面的 SVG 图从“请求路径”视角,展示了三种故障的发生位置与对应的治理策略:
2.6 经典代码示例:互斥锁防止缓存击穿
以下代码展示了使用 Redis SETNX 实现分布式互斥锁,防止热点 key 击穿数据库:
public String getWithMutex(String key) {
// 1. 尝试从缓存获取
String value = redis.get(key);
if (value != null) {
return value;
}
// 2. 缓存未命中,尝试获取分布式锁
String lockKey = "lock:" + key;
boolean locked = redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS);
if (locked) {
try {
// 3. 双重检查:防止在等待锁期间缓存已被重建
value = redis.get(key);
if (value != null) {
return value;
}
// 4. 查询数据库
value = database.query(key);
// 5. 重建缓存(设置随机 TTL 防止雪崩)
int ttl = 300 + new Random().nextInt(60);
redis.setex(key, ttl, value);
return value;
} finally {
// 6. 释放锁
redis.del(lockKey);
}
} else {
// 7. 未获取到锁,等待后重试
Thread.sleep(50);
return getWithMutex(key); // 递归重试
}
}
设计要点:锁的 TTL 必须大于数据库查询耗时(通常设为 2~3 秒);释放锁时务必使用 finally 块,防止异常导致死锁;递归重试需要设置最大重试次数,避免无限循环。
2.7 本章小结
- 缓存穿透的核心治理思路是“拦截不存在 key”,布隆过滤器是首选方案,空值缓存作为兜底。
- 缓存击穿的核心治理思路是“控制热点 key 的回源并发”,互斥锁保证单线程回源,逻辑过期实现无锁异步更新。
- 缓存雪崩的核心治理思路是“分散失效窗口 + 提升缓存可用性”,TTL 随机化、多级缓存和限流熔断缺一不可。
- 三种故障的治理策略并非互斥,在实际生产系统中往往需要组合使用,并通过 OpenAPI 契约将失效策略标准化、可配置化——这正是本课程后续章节将要深入展开的内容。