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 读流程

  1. 应用先查询缓存。
  2. 缓存命中(Cache Hit),直接返回数据。
  3. 缓存未命中(Cache Miss),应用查询数据库。
  4. 将数据库结果写入缓存,并设置过期时间(TTL)。
  5. 返回数据给调用方。

4.2.2 写流程

  1. 应用更新数据库(先写DB)。
  2. 应用删除或失效缓存中的对应键(后删缓存)。
⚠️ 关键陷阱:先删缓存,后写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 流程

  1. 应用请求缓存获取数据。
  2. 缓存未命中时,缓存组件自动查询数据库。
  3. 缓存组件将数据库结果写入自身,再返回给应用。
  4. 应用无需感知数据库的存在。

4.3.2 Write Through 流程

  1. 应用写入缓存。
  2. 缓存组件同步将数据写入数据库。
  3. 只有数据库写入成功,缓存才返回写入成功给应用。
💡 核心优势:应用层代码极简,无需处理缓存失效逻辑。缓存层保证了写入路径的原子性,适合需要强一致性的场景。

4.3.3 适用场景

  • 需要强一致性:如金融交易、库存扣减。
  • 缓存系统提供代理能力:如使用支持Write Through的Redis模块或专用缓存中间件(如Apache Ignite、Hazelcast)。
  • 应用希望解耦:团队希望将缓存策略下沉到基础设施层。

4.4 Write Behind Caching(异步回写)

与Write Through类似,应用只与缓存交互。但区别在于:写入缓存后立即返回成功,缓存异步批量将数据回写到数据库。这是性能最高的策略,但一致性最弱。

4.4.1 流程

  1. 应用写入缓存。
  2. 缓存将写入操作放入内部队列,立即返回成功。
  3. 后台线程或定时任务批量将队列中的数据刷新到数据库。
  4. 如果缓存宕机,队列中未刷新的数据可能丢失。

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 策略选择决策树

在实际架构设计中,可以按以下决策路径选择策略:

  1. 是否需要强一致性?
    • 是 → 考虑 Read/Write Through。
    • 否 → 进入下一步。
  2. 写并发是否极高?
    • 是 → 考虑 Write Behind(如日志、计数)。
    • 否 → 进入下一步。
  3. 团队是否希望简化应用逻辑?
    • 是 → 考虑 Read/Write Through(引入缓存中间件)。
    • 否 → 使用 Cache-Aside(最灵活,最常用)。
📌 工程建议:大多数互联网业务场景下,Cache-Aside + 合理的TTL + 延迟双删 已经能提供足够的一致性保障。只有在写入路径需要事务性保证或写并发极高时,才考虑引入Write Through或Write Behind。
三种缓存更新策略流程对比 Cache-Aside ① 应用读缓存 ② 未命中,读DB ③ 回填缓存+TTL ④ 写DB,删缓存 应用控制失效与回填 最终一致性 Read/Write Through ① 应用读/写缓存 ② 缓存代理查询DB ③ 同步回填/写入DB ④ 返回结果给应用 缓存层封装一致性逻辑 强一致性 Write Behind ① 应用写缓存 ② 写入内部队列 ③ 立即返回成功 ④ 异步批量写DB 异步队列,高性能 弱一致性,有丢失风险 选择依据:一致性要求 → 写并发量 → 团队对缓存层的控制能力 Cache-Aside 最通用;Write Through 最安全;Write Behind 最快