4. 缓存更新策略:Cache-Aside、Read-Through、Write-Through、Write-Behind

在分布式系统中,缓存与数据库之间的数据一致性是核心挑战。不同的缓存更新策略决定了读/写路径如何与后端存储交互,直接影响系统的一致性、延迟和吞吐量。本节深入剖析四种主流策略:Cache-Aside、Read-Through、Write-Through 和 Write-Behind,并结合 Redis 实战场景分析其优劣。

4.1 策略全景对比

下表从读路径、写路径、一致性模型和适用场景四个维度对四种策略进行横向对比:

策略 读路径 写路径 一致性模型 典型场景
Cache-Aside 应用先查缓存,miss 则查 DB 并回填 应用先写 DB,再删除/更新缓存 最终一致性(需处理并发写) 读多写少,业务可容忍短暂不一致
Read-Through 缓存组件自动加载 DB 数据 应用直接写 DB,缓存由失效机制处理 最终一致性(依赖缓存 TTL) 读密集,希望简化应用逻辑
Write-Through 同 Read-Through 应用写缓存,缓存同步写 DB 强一致性(写路径同步) 写后立即读,对一致性要求高
Write-Behind 同 Read-Through 应用写缓存,缓存异步批量写 DB 最终一致性(存在写窗口) 写吞吐量极高,可接受数据丢失风险
关键认知:没有银弹。Cache-Aside 是 Redis 生态中最常用的模式,因为它将缓存与数据库解耦,应用层拥有最大控制权;而 Write-Through 和 Write-Behind 通常需要缓存中间件(如 Apache Ignite、Hazelcast)原生支持,Redis 本身不直接提供这两种模式,但可以通过客户端封装实现。

4.2 Cache-Aside 模式(旁路缓存)

Cache-Aside 是 Redis 应用中最广泛的模式。应用代码显式管理缓存与数据库的交互,Redis 仅作为高速缓存层,不参与持久化决策。

4.2.1 读流程

  1. 应用查询缓存 key。
  2. 如果命中(cache hit),直接返回数据。
  3. 如果未命中(cache miss),查询数据库。
  4. 将数据库结果写入缓存,设置 TTL。
  5. 返回数据给调用方。

4.2.2 写流程

  1. 应用更新数据库(执行 UPDATE/INSERT)。
  2. 删除对应的缓存 key(或更新缓存,但推荐删除)。
常见陷阱:先删除缓存再更新数据库会导致并发读请求在写操作完成前读到旧数据。正确做法是先更新数据库,再删除缓存(即“延迟双删”的简化版)。如果必须保证强一致性,可结合分布式锁或消息队列串行化写操作。
// 伪代码:Cache-Aside 写操作(推荐)
public void updateUser(String userId, User newData) {
    // 1. 先更新数据库
    database.update("users", userId, newData);
    // 2. 再删除缓存(下次读时自动回填)
    redis.del("user:" + userId);
}

// 伪代码:Cache-Aside 读操作
public User getUser(String userId) {
    User user = redis.get("user:" + userId);
    if (user == null) {
        user = database.query("users", userId);
        if (user != null) {
            redis.setex("user:" + userId, 3600, user); // TTL 1小时
        }
    }
    return user;
}

4.3 Read-Through 模式(通读)

Read-Through 将“缓存未命中时自动加载数据库”的逻辑从应用代码下沉到缓存层。应用只与缓存交互,缓存内部封装了数据加载器(Loader)。

  • 优势:应用代码更简洁,无需重复编写 cache-miss 回填逻辑。
  • 劣势:缓存组件需要支持 Loader 接口(如 Redis 的 RedisJSON + RedisGears,或客户端库实现);缓存雪崩时所有请求穿透到 DB 的风险更高。
  • Redis 实现:原生 Redis 不直接支持 Read-Through,但可以通过客户端侧封装(如 Redisson 的 RMapCache 配合 MapLoader)模拟。
// Redisson 示例:Read-Through 配置
MapOptions<String, User> options = MapOptions.<String, User>defaults()
    .loader(new MapLoader<String, User>() {
        @Override
        public User load(String key) {
            return database.query("users", key); // 自动回填
        }
    });
RMapCache<String, User> cache = redisson.getMapCache("users", options);

4.4 Write-Through 模式(通写)

Write-Through 要求每次写操作必须同时更新缓存和数据库,且缓存层负责将数据同步写入 DB。应用只写缓存,缓存保证数据持久化。

  • 一致性:写路径是同步的,缓存与数据库始终一致(不考虑事务回滚场景)。
  • 延迟:每次写操作都包含一次网络 RTT(到缓存)和一次 DB 写入,写延迟较高。
  • 适用:写后立即读的场景,且不能接受脏数据。
Redis 中的 Write-Through 变体:Redis 的 AOF(Append Only File)每次写操作都追加到磁盘,可以看作一种 Write-Through 的持久化策略。但 Redis 作为缓存时通常不开启 AOF 的 always 刷盘模式,因为会严重降低吞吐量。

4.5 Write-Behind 模式(写后/回写)

Write-Behind 允许应用先更新缓存,缓存异步地将变更批量写入数据库。这是四种策略中写吞吐量最高的模式,但也是数据丢失风险最大的模式。

4.5.1 核心特征

  • 异步批处理:缓存层将多个写操作合并,按批次或时间窗口写入 DB。
  • 写窗口:从缓存更新到 DB 写入完成之间存在时间窗口,期间若缓存宕机,数据可能丢失。
  • 幂等性要求:DB 写入操作必须支持幂等,防止重复写入导致数据错误。

4.5.2 适用场景

  • 日志收集、点击流、IoT 传感器数据(可容忍少量丢失)。
  • 秒杀系统的库存扣减(先扣缓存,异步同步 DB)。
  • 排行榜、计数器等高频写场景。
风险提示:Write-Behind 模式下,如果 Redis 主节点宕机且未及时同步到从节点,未落盘的数据将永久丢失。生产环境必须配合 Redis 持久化(RDB/AOF) 和 消息队列(如 Kafka) 做双重保障。
// 伪代码:Write-Behind 异步批量写入
public class WriteBehindCache {
    private final RedisCache cache;
    private final Database db;
    private final ScheduledExecutorService scheduler;
    private final Queue<WriteOperation> buffer = new ConcurrentLinkedQueue<>();

    public void write(String key, Object value) {
        cache.set(key, value); // 先更新缓存
        buffer.offer(new WriteOperation(key, value)); // 入队待写
    }

    @Scheduled(fixedDelay = 1000) // 每秒批量刷一次
    public void flush() {
        List<WriteOperation> batch = new ArrayList<>();
        buffer.drainTo(batch, 1000); // 每次最多取1000条
        if (!batch.isEmpty()) {
            db.batchUpdate(batch); // 批量写入数据库
        }
    }
}

4.6 四种策略的 SVG 流程对比

下图从读路径和写路径两个维度,对比 Cache-Aside、Read-Through、Write-Through 和 Write-Behind 的核心交互流程:

四种缓存更新策略核心流程对比 应用 缓存 数据库 Cache-Aside 应用 缓存 数据库 读 miss 回填 先写 DB 再删缓存 Write-Through 应用 缓存 数据库 写缓存 同步写 DB Write-Behind 应用 缓存 数据库 写缓存 异步批量 刷入 DB Read-Through 应用 缓存 数据库 读缓存 miss 自动加载 查 DB

4.7 策略选型决策树

在实际项目中,选择哪种策略取决于一致性要求、写吞吐量和团队运维能力。以下决策树可帮助快速定位:

  1. 是否需要强一致性?
    • 是 → 考虑 Write-Through(同步写 DB)或 Cache-Aside + 分布式锁。
    • 否 → 进入下一步。
  2. 写吞吐量是否极高(> 10K QPS)?
    • 是 → 考虑 Write-Behind(异步批量写入),需接受数据丢失风险。
    • 否 → 进入下一步。
  3. 是否希望简化应用代码?
    • 是 → 考虑 Read-Through / Write-Through(缓存层封装逻辑)。
    • 否 → 使用 Cache-Aside(最灵活,最可控)。
Redis 实战建议:在大多数互联网业务中,Cache-Aside + 合理 TTL + 延迟双删 是性价比最高的方案。如果写并发极高(如秒杀),可在 Cache-Aside 基础上引入 本地缓存(Caffeine) 或 Write-Behind 队列 做写加速,但务必配合 Redis 持久化和监控告警。

4.8 本章小结

  • Cache-Aside:应用主导,先写 DB 再删缓存,适合大多数场景。
  • Read-Through:缓存自动回填,简化读路径,但需防缓存雪崩。
  • Write-Through:同步写缓存和 DB,一致性最强,写延迟最高。
  • Write-Behind:异步批量写 DB,吞吐量最高,数据丢失风险最大。
  • 选型时需在一致性、延迟、吞吐量和复杂度之间做权衡。