2. 缓存基础回顾:本地缓存 vs 分布式缓存,缓存穿透、缓存击穿、缓存雪崩的定义与经典解法
2.1 本地缓存与分布式缓存:架构选型对比
在服务边界视角下,缓存首先面临一个基础决策:数据应该缓存在应用进程内(本地),还是缓存在独立的外部服务中(分布式)。两种模式在延迟、一致性、容量和运维复杂度上存在本质差异。
| 对比维度 | 本地缓存(Local Cache) | 分布式缓存(Distributed Cache) |
|---|---|---|
| 数据存储位置 | 应用进程堆内 / 堆外内存 | 独立缓存集群(如 Redis、Memcached) |
| 访问延迟 | 纳秒级(无网络开销) | 微秒~毫秒级(网络 RTT 主导) |
| 数据一致性 | 各节点独立,天然不一致 | 可通过主从 / 集群协议保证最终一致性 |
| 容量上限 | 受限于单机堆内存(通常 GB 级) | 可水平扩展至 TB 级 |
| 典型实现 | Guava Cache、Caffeine、Ehcache | Redis Cluster、Redis Sentinel、Memcached |
| 适用场景 | 配置信息、静态字典、热点 Key 本地副本 | 会话状态、全局计数器、跨服务共享数据 |
架构决策提示: 在微服务架构中,本地缓存适合存放“写少读多、变更可容忍短暂不一致”的元数据;分布式缓存则适合需要全局一致视图或跨服务共享的热点数据。两者常组合为“本地缓存兜底 + 分布式缓存主存”的多级缓存模式。
2.2 缓存穿透、击穿、雪崩:定义与经典解法
这三个问题是缓存层最常见的故障模式,理解其本质差异是设计高可用缓存系统的前提。
2.2.1 缓存穿透(Cache Penetration)
定义: 请求查询一个根本不存在的数据(既不在缓存,也不在数据库),导致每次请求都穿透缓存直达数据库,造成数据库压力陡增。
经典解法:
- 空值缓存: 将查询结果为 null 的 Key 也写入缓存,设置较短的 TTL(如 30~60 秒),避免重复穿透。
- 布隆过滤器(Bloom Filter): 在缓存前加一层布隆过滤器,快速判断 Key 是否可能存在。若过滤器判定不存在,直接拒绝请求。
- 参数校验: 在入口层对请求参数进行合法性校验,拦截明显无效的 Key(如 id <= 0)。
2.2.2 缓存击穿(Cache Breakdown)
定义: 一个热点 Key 在缓存中过期失效的瞬间,大量并发请求同时涌入,全部穿透到数据库,导致数据库负载瞬间飙升。
经典解法:
- 互斥锁(Mutex Key): 当缓存失效时,只允许一个线程去数据库加载数据,其他线程等待或重试。常用 Redis SETNX 或本地锁实现。
- 逻辑过期(预加载): 缓存中不设置物理 TTL,而是存储一个逻辑过期时间字段。后台异步线程定期刷新热点数据,保证缓存始终有效。
- 永不过期 + 主动更新: 对极端热点 Key 设置永不过期,通过消息队列或定时任务主动更新缓存内容。
2.2.3 缓存雪崩(Cache Avalanche)
定义: 大量缓存 Key 在同一时间集中过期失效,或缓存节点整体宕机,导致海量请求直接压向数据库,引发连锁故障。
经典解法:
- 过期时间打散(TTL 抖动): 在基础 TTL 上增加随机偏移量(如 ±10%~30%),避免大量 Key 同时过期。
- 缓存高可用: 采用 Redis Sentinel 或 Cluster 模式,避免单点故障;配合多级缓存(本地 + 分布式)降低穿透比例。
- 限流 & 降级: 在数据库访问层配置限流(如 Sentinel、Hystrix),当缓存失效时,部分请求直接返回降级结果或错误提示。
- 提前预热: 在流量高峰期前,手动或自动将热点数据提前加载到缓存中。
记忆口诀: 穿透查“无”,击穿打“热”,雪崩倒“片”。—— 穿透针对不存在的数据,击穿针对单个热点 Key 过期,雪崩针对大量 Key 同时过期或节点宕机。
2.3 三种故障模式的对比与应对策略总览
| 故障模式 | 触发条件 | 影响范围 | 核心应对策略 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 单个 Key 持续穿透 | 空值缓存、布隆过滤器、参数校验 |
| 缓存击穿 | 热点 Key 过期瞬间 | 单个热点 Key 高并发穿透 | 互斥锁、逻辑过期、永不过期 + 主动更新 |
| 缓存雪崩 | 大量 Key 同时过期 / 节点宕机 | 大面积缓存失效 | TTL 打散、缓存高可用、限流降级、预热 |
2.4 缓存故障模式与应对策略流程图
2.5 代码示例:互斥锁防止缓存击穿
以下代码展示了使用 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 = db.query(key);
redis.setex(key, 300, value);
return value;
} finally {
redis.del(lockKey);
}
} else {
// 5. 未获取到锁,等待后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getWithMutex(key); // 递归重试
}
}
注意事项: 互斥锁方案会引入额外的锁等待时间,适用于并发量极高但数据库负载敏感的场景。对于可以容忍短暂不一致的业务,逻辑过期方案通常具有更好的吞吐性能。