3. 缓存模式概述:Cache-Aside、Read-Through、Write-Through、Write-Behind 模式对比
在构建高并发、低延迟的 Redis 缓存系统时,选择正确的缓存模式是架构设计的核心决策。不同的模式决定了应用程序与缓存、数据库之间的交互方式,直接影响数据一致性、性能瓶颈和运维复杂度。本章将系统对比四种主流缓存模式:Cache-Aside、Read-Through、Write-Through 和 Write-Behind,帮助你在契约设计中做出合理选择。
3.1 四种模式核心概念
| 模式 | 读操作策略 | 写操作策略 | 缓存与数据库一致性 | 典型适用场景 |
|---|---|---|---|---|
| Cache-Aside | 应用先读缓存,未命中则读数据库并回填缓存 | 应用直接写数据库,同时删除或更新缓存 | 最终一致(需处理并发写冲突) | 读多写少、业务逻辑灵活控制 |
| Read-Through | 缓存层自动回填(由缓存组件负责) | 应用直接写数据库,缓存失效 | 最终一致(读路径自动修复) | 读密集型、缓存层封装度高 |
| Write-Through | 同 Read-Through | 应用写缓存,缓存同步写数据库 | 强一致(写路径同步) | 写一致性要求高、数据不丢失 |
| Write-Behind | 同 Read-Through | 应用写缓存,缓存异步批量写数据库 | 最终一致(存在写回延迟) | 写吞吐极高、允许短时数据不一致 |
架构师提示:在 Redis 缓存契约设计中,Cache-Aside 是最灵活的模式,适合大多数业务场景;Write-Through 适合金融、订单等强一致场景;Write-Behind 适合日志、埋点等写吞吐量巨大的场景。
3.2 Cache-Aside 模式详解
Cache-Aside(旁路缓存)是应用最广泛的模式。应用程序负责管理缓存的读写逻辑,缓存层不直接与数据库交互。
- 读流程:应用先查询缓存 → 命中则直接返回 → 未命中则查询数据库 → 将结果写入缓存 → 返回给调用方。
- 写流程:应用先更新数据库 → 然后删除(或更新)缓存中的对应键。
- 一致性保障:采用“先更新数据库,后删除缓存”策略,配合缓存过期时间,可达到最终一致性。
// Cache-Aside 读操作伪代码
public Object get(String key) {
Object value = redis.get(key);
if (value != null) {
return value; // 缓存命中
}
// 缓存未命中,从数据库加载
value = database.query(key);
if (value != null) {
redis.set(key, value, 300); // 设置过期时间
}
return value;
}
// Cache-Aside 写操作伪代码
public void update(String key, Object newValue) {
database.update(key, newValue); // 先更新数据库
redis.del(key); // 再删除缓存
}
常见陷阱:并发写场景下,如果先删除缓存再更新数据库,会导致其他线程读到旧数据。务必采用“先更新数据库,后删除缓存”的顺序,或使用分布式锁。
3.3 Read-Through 模式详解
Read-Through(通读)模式将缓存未命中的回填逻辑下沉到缓存层。应用程序只与缓存交互,缓存组件(如 Redis 配合自定义加载器)负责从数据库加载数据。
- 读流程:应用请求缓存 → 缓存未命中 → 缓存组件自动调用数据库加载器 → 加载数据并回填缓存 → 返回给应用。
- 写流程:应用直接写数据库,或通过缓存失效机制触发下次读时重新加载。
- 优势:应用代码简化,缓存层封装了数据加载逻辑,便于统一管理缓存预热和失效策略。
// Read-Through 模式示意(Redis 配合自定义加载器)
// 应用代码只需调用缓存接口
public Object get(String key) {
// 缓存层内部自动处理未命中回填
return redis.getWithLoader(key, loader -> {
return database.query(key);
});
}
3.4 Write-Through 模式详解
Write-Through(通写)模式要求每次写操作都必须同步写入缓存和数据库。缓存层作为写入的代理,保证数据在缓存和数据库中的强一致性。
- 写流程:应用写缓存 → 缓存同步写数据库 → 两者都成功后才返回成功。
- 读流程:同 Read-Through,缓存未命中时自动回填。
- 一致性:强一致,但写延迟较高(因为需要等待数据库写入完成)。
// Write-Through 写操作伪代码
public void write(String key, Object value) {
// 缓存层保证原子性写入
redis.set(key, value);
database.update(key, value); // 同步写入数据库
// 只有两者都成功,才返回成功
}
性能考量:Write-Through 模式适合写频率不高但要求数据不丢失的场景。如果写并发高,数据库会成为瓶颈,建议改用 Write-Behind 模式。
3.5 Write-Behind 模式详解
Write-Behind(写后)模式将写操作先写入缓存,然后异步批量写入数据库。缓存层负责积累写操作,在后台按批次或定时策略同步到数据库。
- 写流程:应用写缓存 → 立即返回成功 → 后台线程异步批量写入数据库。
- 读流程:同 Read-Through,缓存未命中时自动回填。
- 优势:写吞吐极高,适合日志、埋点、计数器等场景。
- 风险:缓存宕机可能导致数据丢失,需要持久化或副本机制保障。
// Write-Behind 写操作伪代码
public void write(String key, Object value) {
redis.set(key, value); // 先写入缓存
// 后台异步批量写入数据库
asyncBatchQueue.add(new WriteTask(key, value));
}
// 后台线程定时刷盘
public void flushToDatabase() {
List<WriteTask> batch = asyncBatchQueue.drainAll();
database.batchUpdate(batch); // 批量写入
}
3.6 四种模式对比总结
下表从多个维度对四种模式进行横向对比,帮助你在契约设计中快速决策:
| 对比维度 | Cache-Aside | Read-Through | Write-Through | Write-Behind |
|---|---|---|---|---|
| 读延迟 | 低(命中时) | 低(命中时) | 低(命中时) | 低(命中时) |
| 写延迟 | 低(仅操作数据库+缓存删除) | 低(仅操作数据库) | 高(需等待数据库写入) | 极低(仅操作缓存) |
| 数据一致性 | 最终一致 | 最终一致 | 强一致 | 最终一致(有窗口期) |
| 缓存与数据库耦合 | 低(应用控制) | 中(缓存层控制读) | 高(缓存层控制读写) | 高(缓存层控制读写) |
| 实现复杂度 | 低 | 中 | 中 | 高(需异步队列、容错) |
| 适用写吞吐 | 中 | 中 | 低 | 极高 |
| 缓存宕机影响 | 可恢复(数据库有全量数据) | 可恢复 | 可恢复(数据库有全量数据) | 可能丢失未刷盘数据 |
3.7 四种模式架构对比图
下图展示了四种缓存模式的核心架构差异,重点关注应用、缓存、数据库之间的数据流向:
3.8 模式选择决策流程
在实际项目中,选择哪种缓存模式取决于业务对一致性、延迟和吞吐的要求。以下决策流程可以帮助你快速定位:
3.9 契约设计中的模式选择建议
架构师最佳实践:
- 默认选择 Cache-Aside:大多数业务系统适用,应用层控制灵活,易于调试和扩展。
- 读密集型且缓存层封装度高:选择 Read-Through,减少应用层重复代码。
- 写一致性要求严格(如订单、支付):选择 Write-Through,确保缓存与数据库强一致。
- 写吞吐极高(如日志、埋点、计数器):选择 Write-Behind,配合 Redis 持久化(AOF/RDB)降低数据丢失风险。
- 混合使用:同一系统可按业务维度混合使用不同模式,例如订单用 Write-Through,商品详情用 Cache-Aside。