2. 缓存基础回顾:缓存的作用与常见模式
2.1 缓存的核心作用
在分布式系统中,缓存位于数据库与应用程序之间,通过将热点数据存储在高速存储介质(通常是内存)中,显著降低数据访问延迟并减轻后端数据库的负载。其核心价值体现在三个维度:
- 降低延迟:内存访问延迟通常在微秒级,而磁盘 I/O 在毫秒级,缓存可将读操作延迟降低 1~3 个数量级。
- 提升吞吐:缓存层可承受每秒数万至数十万次查询,有效分担数据库连接池与磁盘 I/O 压力。
- 削峰填谷:在流量突发场景下,缓存吸收瞬时请求洪峰,防止数据库因连接耗尽而崩溃。
关键指标:缓存命中率是衡量缓存效率的核心指标。生产环境中,读密集型业务通常要求缓存命中率 ≥ 95%;低于 85% 时需排查缓存策略或数据预热逻辑。
2.2 三种主流缓存模式
不同的缓存模式决定了应用程序与缓存、数据库之间的交互方式,也直接影响缓存一致性的维护成本。下表对比了三种常见模式的核心差异:
| 模式 | 读操作流程 | 写操作流程 | 一致性维护 | 适用场景 |
|---|---|---|---|---|
| Cache-Aside | 应用先读缓存,未命中则读 DB 并回填缓存 | 应用先写 DB,再删除或更新缓存 | 应用层负责,需处理并发写冲突 | 通用场景,灵活性最高 |
| Read-Through | 缓存组件自动回填(应用不直接读 DB) | 同 Cache-Aside 或由缓存代理 | 缓存层封装,对应用透明 | 读密集、缓存层可扩展 |
| Write-Through | 同 Read-Through | 应用写缓存,缓存同步写 DB | 缓存层保证强一致(写路径) | 写后立即读、一致性要求高 |
2.2.1 Cache-Aside(旁路缓存)
Cache-Aside 是业界最广泛采用的模式,应用程序直接管理缓存与数据库的读写路径。其核心逻辑如下:
// 读操作
public String get(String key) {
String value = cache.get(key);
if (value == null) {
value = db.query(key); // 缓存未命中,查数据库
cache.set(key, value, ttl); // 回填缓存,设置过期时间
}
return value;
}
// 写操作
public void update(String key, String newValue) {
db.update(key, newValue); // 先更新数据库
cache.delete(key); // 再删除缓存(或更新)
}
一致性陷阱:Cache-Aside 在并发写场景下存在「先删缓存后写 DB」的时序问题,可能导致脏数据。标准做法是「先写 DB 后删缓存」,并配合延迟双删或消息队列补偿。
2.2.2 Read-Through(通读缓存)
Read-Through 模式下,缓存层(如 Redis Cluster + 自定义加载器)接管了数据回填逻辑。应用程序只与缓存交互,缓存未命中时由缓存组件自动查询数据库并填充。该模式对应用层更透明,但要求缓存层具备数据加载能力。
2.2.3 Write-Through(通写缓存)
Write-Through 模式下,每次写操作都先写入缓存,再由缓存同步写入数据库。该模式保证了写路径的强一致性,但写延迟较高(需等待数据库确认),且不适合写频繁的业务。
2.3 缓存三大经典问题
缓存穿透、缓存击穿、缓存雪崩是分布式缓存运维中最常见的三类故障模式。理解其成因与应对策略,是设计高可用缓存系统的前提。
| 问题 | 现象 | 根因 | 典型解决方案 |
|---|---|---|---|
| 缓存穿透 | 请求绕过缓存直接打到数据库 | 查询不存在的数据(key 在缓存和 DB 中均无) | 布隆过滤器、缓存空对象(短 TTL) |
| 缓存击穿 | 热点 key 过期瞬间,大量并发请求直击 DB | 单个热点 key 过期 + 高并发 | 互斥锁(Mutex)、逻辑过期、永不过期 + 异步刷新 |
| 缓存雪崩 | 大量 key 同时过期或缓存节点宕机,DB 被压垮 | 批量 key 同时过期 / 缓存集群故障 | 过期时间加随机偏移、缓存集群高可用(哨兵/集群)、限流降级 |
2.3.1 缓存穿透
当请求的 key 在缓存和数据库中都不存在时,每次请求都会穿透缓存直达数据库。攻击者可以利用此漏洞构造大量不存在的 key 进行恶意请求,导致数据库负载飙升。
防御手段:
- 布隆过滤器:在缓存前加一层布隆过滤器,判断 key 是否存在,不存在则直接拒绝。
- 缓存空对象:对查询结果为空的 key 也缓存一个占位符(TTL 设为 30~60 秒),防止重复穿透。
2.3.2 缓存击穿
缓存击穿特指某个热点 key 在过期瞬间,大量并发请求同时发现缓存失效,全部回源查询数据库,导致数据库瞬间压力过大。
推荐方案:使用互斥锁(如 Redis SETNX)保证只有一个线程去加载数据,其他线程等待或快速失败。对于读多写少的场景,可采用「逻辑过期」策略——缓存永不过期,但数据中携带过期时间,异步线程负责刷新。
2.3.3 缓存雪崩
缓存雪崩有两种典型诱因:一是大量 key 设置了相同的过期时间,在同一时刻集体失效;二是缓存节点宕机导致整个缓存层不可用。两种情况下,所有请求都会直接压向后端数据库。
工程实践:
- 设置过期时间时,在基础 TTL 上增加 ±10%~30% 的随机偏移,避免批量失效。
- 缓存集群必须部署哨兵或 Cluster 模式,保证故障自动切换。
- 在应用层配置限流(如 Sentinel)和熔断降级策略,防止数据库被击穿。
2.4 缓存一致性问题的根源
缓存与数据库之间的数据不一致,本质上是写操作时序与并发访问共同作用的结果。在 Cache-Aside 模式下,典型的冲突场景包括:
- 先删缓存后写 DB:线程 A 删除缓存后,线程 B 读取旧数据并回填缓存,导致缓存中为旧值。
- 先写 DB 后更新缓存:写 DB 成功但更新缓存失败,缓存仍为旧数据。
- 并发写冲突:两个线程同时写 DB,后写 DB 的线程先更新缓存,导致缓存与 DB 最终状态不一致。