4、缓存模式(上):Cache-Aside 模式详解、Read-Through 模式、Write-Through 模式

聊到缓存模式,很多人第一反应就是「缓存不就是在数据库前面加一层 Redis 吗?」。嗯,这话没错,但太粗糙了。你想想看,缓存和数据库之间的数据怎么同步?谁来负责加载?谁来负责写入?这些细节决定了你的系统是稳定高效,还是三天两头出数据不一致的 Bug。

我个人习惯把缓存模式分成两大类:应用端主动管理 和 缓存层自动管理。今天我们先聊三个最基础也最常用的模式:Cache-Aside、Read-Through 和 Write-Through。别小看它们,我见过不少团队用了好几年 Redis,其实一直在用 Cache-Aside,只是自己不知道这个叫法而已。

三种缓存模式核心流程对比 三种缓存模式核心流程对比 Cache-Aside 应用 (Application) 缓存 (Cache) 数据库 (DB) ① 读缓存 ② 未命中则读DB ③ 回写缓存 应用负责所有读写逻辑 Read-Through 应用 (Application) 缓存 (Cache) 数据库 (DB) ① 读缓存 ② 缓存自动读DB ③ 自动回写缓存 缓存层自动加载数据 Write-Through 应用 (Application) 缓存 (Cache) 数据库 (DB) ① 写缓存 ② 缓存自动写DB ③ 返回成功 写操作必须经过缓存

Cache-Aside:最常用的缓存模式

Cache-Aside,也叫「旁路缓存」。说白了,就是应用自己管理缓存的读写。这是最灵活的模式,也是我工作中用得最多的。

它的核心流程很简单:

  1. 读请求:先查缓存,命中就直接返回;没命中就去查数据库,然后把结果写回缓存,再返回。
  2. 写请求:先更新数据库,然后删除缓存(或者更新缓存)。

这里有个关键点——为什么写的时候是「删除缓存」而不是「更新缓存」?

我曾经在一个电商项目中踩过这个坑。当时我们写订单数据时直接更新缓存,结果并发场景下出现了「脏写」:两个线程同时更新,后写的反而覆盖了先写的正确数据。从那以后,我团队里的代码规范就加了一条:Cache-Aside 模式下,写操作一律删除缓存,不做原地更新。

核心原则:Cache-Aside 的写操作,优先删除缓存而非更新缓存。这样可以避免并发写导致的脏数据问题。

来看一段伪代码,这是我个人习惯的写法:

// 读操作
public User getUser(String userId) {
    // 1. 先查缓存
    User user = cache.get("user:" + userId);
    if (user != null) {
        return user;
    }
    
    // 2. 缓存未命中,查数据库
    user = db.query("SELECT * FROM users WHERE id = ?", userId);
    
    // 3. 回写缓存(设置过期时间)
    if (user != null) {
        cache.set("user:" + userId, user, 3600);
    }
    
    return user;
}

// 写操作
public void updateUser(User user) {
    // 1. 先更新数据库
    db.execute("UPDATE users SET name = ? WHERE id = ?", user.getName(), user.getId());
    
    // 2. 删除缓存(不是更新!)
    cache.del("user:" + user.getId());
}

小技巧:回写缓存时一定要设置 TTL(过期时间)。我见过有人不设过期时间,结果缓存里堆满了冷数据,内存被打满,Redis 直接 OOM。设个合理的 TTL,既保证数据最终一致性,又能自动清理冷数据。

Read-Through:让缓存层帮你干活

Read-Through 和 Cache-Aside 很像,但有一个本质区别:加载数据的逻辑从应用移到了缓存层。

在 Read-Through 模式下,应用只跟缓存打交道。缓存没命中时,缓存组件(比如 Redis 的客户端库或代理层)会自动去数据库加载数据,然后返回给应用。应用根本不知道数据库的存在——至少从代码层面看是这样。

这样做的好处很明显:

  • 代码更干净:应用层不需要写「查缓存→查数据库→回写缓存」这一套模板代码。
  • 逻辑统一:数据加载策略集中在缓存层管理,修改起来更方便。

但缺点也很实在:

  • 灵活性降低:复杂的查询逻辑很难在缓存层表达。比如你需要做多表联查、聚合计算,Read-Through 就有点力不从心了。
  • 延迟敏感:缓存层加载数据时如果数据库响应慢,会直接拖垮缓存层的线程池。

我记得之前帮一个团队做架构评审,他们用了一个开源的缓存代理组件来实现 Read-Through。结果上线后频繁超时,一查才发现缓存代理的数据库连接池配得太小,并发一高就全堵住了。说白了,Read-Through 是把双刃剑——用好了代码简洁,用不好就是新的瓶颈点。

注意:Read-Through 模式适合「读多写少」且查询逻辑相对固定的场景。如果你的查询条件千变万化,或者需要做复杂的业务计算,建议还是用 Cache-Aside,把控制权留在应用层。

Write-Through:写入必须经过缓存

Write-Through 模式,顾名思义——写操作必须经过缓存。应用只写缓存,缓存负责把数据同步到数据库。

这个模式的核心流程:

  1. 应用向缓存发起写请求。
  2. 缓存先更新自身数据,然后同步写入数据库。
  3. 只有数据库写入成功,缓存才返回「写入成功」给应用。

你可能会问:「这不就是多此一举吗?直接写数据库不就行了?」

嗯,这里的关键在于数据一致性。Write-Through 保证缓存和数据库的数据是强一致的——要么都成功,要么都失败。对于某些对一致性要求极高的场景(比如金融交易、库存扣减),这个模式很有价值。

但代价也很明显:

  • 写入延迟增加:每次写操作都要等数据库确认,相当于在缓存后面又串了一个同步操作。
  • 缓存压力变大:所有写流量都先打到缓存,缓存成了写入路径上的单点。

我曾经在一个支付系统中用过 Write-Through。当时的需求是「余额变更必须实时同步到数据库,不能有任何延迟」。我们用了 Write-Through 模式,配合本地事务,确保缓存和数据库的写入在同一个事务边界内。效果不错,但代价是写入性能比直接写数据库慢了 20% 左右。

适用场景总结:Write-Through 适合「写操作频繁但数据一致性要求极高」的场景。如果你的业务允许秒级的数据不一致,用 Cache-Aside + 异步删除缓存就足够了,没必要上 Write-Through 增加复杂度。

三种模式对比

说了这么多,我们来个直观的对比:

特性 Cache-Aside Read-Through Write-Through
谁负责加载数据 应用 缓存层 缓存层
谁负责写入数据库 应用 应用(读由缓存层负责) 缓存层
数据一致性 最终一致(需处理并发) 最终一致 强一致(事务内)
代码复杂度 中等(需写模板代码) 低(应用层简单) 低(应用层简单)
写入性能 高(直接写DB) 高 中(需等待DB确认)
适用场景 大多数业务系统 读多写少、查询固定 强一致性要求高

我的建议:如果你刚开始做缓存设计,先从 Cache-Aside 入手。它最灵活,也最容易排查问题。等你对业务和数据流有了足够理解,再考虑 Read-Through 或 Write-Through 来简化代码或提升一致性。

这三种模式是缓存世界的基石。你想想看,不管多复杂的缓存架构,底层无非就是「谁来读、谁来写、谁来同步」这三个问题的不同组合。理解了它们,后面讲 Write-Behind、Refresh-Ahead 这些进阶模式时,你就能更快上手了。