3. 缓存更新策略全景

缓存一致性问题的本质,是同一份数据同时存在于数据库与缓存两个位置,而任何一次写操作都必须在两者之间做出“谁先谁后、谁负责同步”的取舍。围绕这个取舍,工程上形成了三类主流更新策略:Cache Aside(旁路缓存)、Read/Write Through(读写穿透)与Write Behind(回写/写后写)。它们并不是互斥的“对错方案”,而是面向不同读写比例、一致性容忍度和实现复杂度的不同边界选择。

一、Cache Aside:应用层主导的双写编排

Cache Aside 是最常见的策略,其核心特征是:缓存不主动感知数据库,所有读写编排由应用代码完成。读路径为“先查缓存,命中则返回;未命中则查数据库,再回填缓存”。写路径的经典做法是“先更新数据库,再删除缓存”,而不是更新缓存。

为什么写路径推荐“删除”而非“更新”?因为更新缓存需要应用自己计算新值,若并发写导致计算顺序与落库顺序不一致,缓存可能被写入旧值;而删除缓存把“重新加载”的责任交给下一次读,天然避免了这一风险。下面是一个典型的读回填与写删除示例:

// 读:Cache Aside
String get(String key) {
    String v = redis.get(key);
    if (v != null) return v;              // 命中
    v = db.query(key);                    // 未命中,回源
    if (v != null) redis.setex(key, 300, v); // 回填,TTL 300s
    return v;
}

// 写:先更新库,再删除缓存
void update(String key, String val) {
    db.update(key, val);                  // 1. 落库
    redis.del(key);                       // 2. 删除缓存
}

Cache Aside 的边界在于:它不保证强一致。在“读未命中回填”与“写删除”并发时,可能出现旧值被回填的窗口。缓解手段包括:给缓存设置合理 TTL 作为兜底、对同一 key 的读写加分布式锁、或采用“延迟双删”(更新库后删一次,短暂延迟后再删一次)。它适合读多写少、能接受最终一致、且希望实现简单的业务,例如商品详情、用户资料。

二、Read/Write Through:缓存层代理数据库访问

Read/Write Through 把同步责任从应用下沉到缓存层。应用只与缓存交互,缓存组件自身负责在未命中时从数据库加载(Read Through),以及在写入时同步更新数据库(Write Through)。应用代码不再出现“先查库再回填”的编排逻辑。

两者的关键差异是:Cache Aside 中“回源”由应用完成,缓存是被动存储;Read/Write Through 中“回源与落库”由缓存代理完成,缓存是主动的数据访问层。这带来更干净的调用方代码,但要求缓存组件具备数据库连接与映射能力,常见于本地缓存框架(如 Guava/Caffeine 配合自定义 Loader)或部分托管缓存服务。

Write Through 的写路径是“写缓存的同时写数据库”,因此写延迟包含数据库写入耗时,吞吐受数据库限制。它适合读多写少、且希望应用层逻辑统一的场景,但实现成本高于 Cache Aside。

三、Write Behind:异步批量回写换取吞吐

Write Behind(又称 Write Back)在 Write Through 基础上进一步解耦:写操作只更新缓存并标记为“脏”,由后台线程异步、批量地将脏数据刷回数据库。它的优势是写延迟极低、可合并同一 key 的多次写,显著提升写吞吐;代价是一致性最弱——缓存已更新而数据库尚未落盘,此时若缓存节点宕机,未刷回的数据会丢失。

因此 Write Behind 的适用边界非常明确:适合写密集、可容忍短暂数据丢失、且能接受最终一致的场景,例如计数器、点赞数、会话状态、监控指标聚合。使用时通常需要配合持久化队列或 WAL,降低宕机丢数据风险。

三种缓存更新策略的读写路径对比 Cache Aside 应用层编排 · 先库后删缓存 应用代码 Redis MySQL 读/写 回源/落库 回填 写路径 1. 更新数据库 2. 删除缓存 key 3. 下次读时回填 一致性:最终一致 写延迟:低(仅删缓存) 适用:读多写少 Read/Write Through 缓存层代理 · 同步落库 应用代码 缓存层(含 Loader/Writer) MySQL 读写请求 同步加载/写入 特点 应用不感知数据库 写延迟含库写入耗时 一致性:较强(同步) 适用:读多写少、逻辑统一 Write Behind 异步批量回写 · 脏标记 应用代码 缓存(脏数据队列) MySQL 写请求 异步批量刷回 特点 写延迟极低 可合并同 key 多次写 一致性:最弱(可能丢数据) 适用:写密集、可容忍丢失

四、如何选择:从一致性、延迟与复杂度三个维度判断

三种策略的取舍可以归纳为一张判断表。需要强调的是,一致性越强,通常写延迟越高、实现越复杂;反之,吞吐越高,一致性越弱。

策略写路径一致性写延迟实现复杂度典型场景
Cache Aside更新库 → 删缓存最终一致低低商品详情、用户资料
Read/Write Through缓存同步写库较强中(含库耗时)中统一访问层、读多写少
Write Behind写缓存 → 异步刷库最弱极低高计数器、点赞、会话状态
选择建议:若业务能接受最终一致且希望快速落地,优先 Cache Aside;若希望应用层不感知数据库、且写量不大,可考虑 Read/Write Through;只有在写吞吐是瓶颈、且能接受短暂数据丢失时,才引入 Write Behind,并务必配合持久化队列或 WAL 降低风险。
常见误区:把 Write Behind 当作“高性能的 Cache Aside”使用,却忽略了它可能丢数据;或在 Cache Aside 中采用“更新缓存”而非“删除缓存”,在并发写下引入旧值覆盖。策略选择必须与业务的一致性容忍度对齐,而不是只看性能数字。

理解这三种策略的流程与边界,是后续讨论延迟双删、分布式锁、消息队列补偿等具体一致性方案的基础。实际工程中,同一系统内不同数据往往采用不同策略:核心交易数据用 Cache Aside 加锁保证,统计类数据用 Write Behind 提升吞吐,这正是“策略全景”真正的价值所在。