4. 缓存更新策略:Cache-Aside、Read/Write Through、Write Behind Caching 原理与对比
在高并发系统中,缓存与数据库之间的数据一致性是核心挑战。选择错误的更新策略可能导致缓存雪崩、数据不一致或性能瓶颈。本章深入剖析三种主流缓存更新策略的原理、适用场景与工程取舍。
4.1 策略全景与核心权衡
所有缓存更新策略都在解决同一个问题:当数据在数据库中发生变更时,如何同步更新缓存,以保证后续读取不会返回脏数据? 三种策略的核心差异在于:谁负责协调写入顺序(应用还是缓存系统)以及写入是同步还是异步。
| 策略 | 协调者 | 写入时机 | 一致性强度 | 对缓存系统的依赖 |
|---|---|---|---|---|
| Cache-Aside | 应用程序 | 读取时按需加载 | 最终一致性(需配合过期) | 低(仅需基本读写API) |
| Read/Write Through | 缓存层(代理) | 写入时同步穿透 | 强一致性(单点写入路径) | 高(需支持代理逻辑) |
| Write Behind | 缓存层(异步队列) | 写入后异步回写 | 弱一致性(有丢失窗口) | 极高(需持久化与队列) |
4.2 Cache-Aside(旁路缓存)
这是最广泛使用的策略,应用程序直接管理缓存与数据库的交互。核心原则是:读时回填,写时失效。
4.2.1 读流程
- 应用先查询缓存。
- 缓存命中(Cache Hit),直接返回数据。
- 缓存未命中(Cache Miss),应用查询数据库。
- 将数据库结果写入缓存,并设置过期时间(TTL)。
- 返回数据给调用方。
4.2.2 写流程
- 应用更新数据库(先写DB)。
- 应用删除或失效缓存中的对应键(后删缓存)。
⚠️ 关键陷阱:先删缓存,后写DB
如果先删除缓存,在写入DB之前,另一个线程可能读取到旧数据并回填缓存,导致缓存中永久保留脏数据。正确的顺序是先写DB,后删缓存,利用下一次读取来填充新数据。
如果先删除缓存,在写入DB之前,另一个线程可能读取到旧数据并回填缓存,导致缓存中永久保留脏数据。正确的顺序是先写DB,后删缓存,利用下一次读取来填充新数据。
// Cache-Aside 写操作伪代码(正确顺序)
public void updateUser(User user) {
// 1. 先更新数据库
database.update(user);
// 2. 再删除缓存(延迟双删可进一步保障)
cache.delete("user:" + user.getId());
// 可选:延迟几百毫秒后再删一次,应对主从延迟
}
4.2.3 适用场景
- 读多写少:缓存命中率高,写操作代价可控。
- 数据一致性要求不极端:允许短时间(TTL窗口内)的不一致。
- 缓存系统简单:如Redis/Memcached,无需额外代理层。
4.3 Read/Write Through(读写穿透)
缓存层充当数据库的代理,应用程序只与缓存交互,缓存负责与数据库同步。这种策略将一致性逻辑封装在缓存内部,对应用透明。
4.3.1 Read Through 流程
- 应用请求缓存获取数据。
- 缓存未命中时,缓存组件自动查询数据库。
- 缓存组件将数据库结果写入自身,再返回给应用。
- 应用无需感知数据库的存在。
4.3.2 Write Through 流程
- 应用写入缓存。
- 缓存组件同步将数据写入数据库。
- 只有数据库写入成功,缓存才返回写入成功给应用。
💡 核心优势:应用层代码极简,无需处理缓存失效逻辑。缓存层保证了写入路径的原子性,适合需要强一致性的场景。
4.3.3 适用场景
- 需要强一致性:如金融交易、库存扣减。
- 缓存系统提供代理能力:如使用支持Write Through的Redis模块或专用缓存中间件(如Apache Ignite、Hazelcast)。
- 应用希望解耦:团队希望将缓存策略下沉到基础设施层。
4.4 Write Behind Caching(异步回写)
与Write Through类似,应用只与缓存交互。但区别在于:写入缓存后立即返回成功,缓存异步批量将数据回写到数据库。这是性能最高的策略,但一致性最弱。
4.4.1 流程
- 应用写入缓存。
- 缓存将写入操作放入内部队列,立即返回成功。
- 后台线程或定时任务批量将队列中的数据刷新到数据库。
- 如果缓存宕机,队列中未刷新的数据可能丢失。
4.4.2 关键参数
| 参数 | 说明 | 典型值 |
|---|---|---|
| 队列大小 | 内存中最多积压多少条待回写记录 | 1000 ~ 10000 |
| 刷新间隔 | 定时回写的时间窗口 | 100ms ~ 5s |
| 批量大小 | 每次批量写入数据库的记录数 | 50 ~ 500 |
⚠️ 风险提示:Write Behind 适用于可以容忍数据丢失或可以重建的场景(如页面浏览量、日志聚合)。对于关键业务数据,必须配合持久化队列(如Kafka)或WAL(Write-Ahead Log)来降低丢失风险。
4.5 三种策略对比总结
| 维度 | Cache-Aside | Read/Write Through | Write Behind |
|---|---|---|---|
| 实现复杂度 | 中等(应用需处理失效逻辑) | 低(应用无感知) | 高(需队列与持久化) |
| 读性能 | 高(直接读缓存) | 高(缓存代理) | 高(缓存代理) |
| 写性能 | 中(需写DB+删缓存) | 中(同步写DB) | 极高(异步批量写) |
| 一致性 | 最终一致(TTL窗口) | 强一致(同步写) | 弱一致(有丢失窗口) |
| 缓存雪崩风险 | 低(失效后回填) | 低(缓存代理控制) | 中(队列积压可能) |
| 典型场景 | 用户信息、商品详情 | 库存、账户余额 | 访问计数、日志、排行榜 |
4.6 策略选择决策树
在实际架构设计中,可以按以下决策路径选择策略:
- 是否需要强一致性?
- 是 → 考虑 Read/Write Through。
- 否 → 进入下一步。
- 写并发是否极高?
- 是 → 考虑 Write Behind(如日志、计数)。
- 否 → 进入下一步。
- 团队是否希望简化应用逻辑?
- 是 → 考虑 Read/Write Through(引入缓存中间件)。
- 否 → 使用 Cache-Aside(最灵活,最常用)。
📌 工程建议:大多数互联网业务场景下,Cache-Aside + 合理的TTL + 延迟双删 已经能提供足够的一致性保障。只有在写入路径需要事务性保证或写并发极高时,才考虑引入Write Through或Write Behind。