Cache-aside 将缓存逻辑放在应用层。Read-through 和 write-through 将逻辑移到缓存层本身,因此应用像对待数据库一样与缓存通信,由缓存负责从后备存储加载数据并写入数据。差异看似微妙,却改变了逻辑归属、缓存一致性以及复杂性所在位置。掌握这三种模式,能让你针对每个用例在一致性与复杂性之间做出正确的权衡。

本文是《Redis 大师班》的第 10 部分,承接cache-aside

Read-through:缓存未命中时自动加载

在 read-through 模式下,应用始终从缓存读取数据;缓存负责在未命中时从数据库获取数据。cache-aside 由应用执行“从数据库加载并填充缓存”的步骤,而 read-through 将这一操作封装在缓存层中:

// 应用代码只需简单地从缓存读取
const user = await cache.get(`user:${id}`);
// 缓存层在未命中时,从数据库加载、存储并返回结果

Enter fullscreen mode Exit fullscreen mode

从外部看,行为与 cache-aside 几乎相同:延迟加载、TTL、未命中时填充缓存。区别在于逻辑的位置。read-through 需要缓存能够调用你的数据库(通过提供者、库或你自行封装的包装器),因此加载逻辑集中在一处,而非散布在每个调用点。

优点是实现一致:每次读取都走相同的加载路径,不会出现忘记填充缓存的情况。代价是需要具备加载能力的缓存层。实践中,许多团队通过把 cache-aside 封装成一个共享函数来获得 read-through 的好处——同样能集中逻辑,但无需特殊基础设施。

Write-through:缓存同步写入数据库

write-through 与 read-through 搭配,负责写操作。应用写入缓存,缓存在返回确认前同步写入数据库:

// 应用写入缓存,缓存同步写入数据库
await cache.set(`user:${id}`, updatedUser);
// 内部实现:缓存更新 Redis 并写入数据库,然后返回

Enter fullscreen mode Exit fullscreen mode

缓存和数据库在一次操作中同时更新,因此缓存始终与数据库保持一致(不会因为忘记失效而出现陈旧数据)。每次写入都保持两者同步。

代价是写入延迟。因为写入会同步到达缓存和数据库,写入速度与数据库写入速度相同。你把缓存加入了写入路径,因此写入不会更快(读取会更快)。而且无论数据是否会被读取,都会在写入时被缓存,可能导致缓存中充满冷数据。

Read-through + write-through:一致但写入不快

两者结合使用时,缓存会自动与数据库保持一致:读取未命中时填充,写入时同步更新两个存储。应用把缓存当作数据接口,缓存自行保持同步。当你希望在不分散失效逻辑的情况下获得强一致性时,这种模式很有吸引力。

诚实的局限:写入不会更快(需要命中两个存储),缓存会保存可能永远不会被读取的数据(每次写入都缓存),并且需要缓存层支持该模式。它适用于读多写少且一致性重要的场景,能把缓存逻辑集中管理;对简单的缓存场景来说,cache-aside 的写入即删策略已经足够。

三种模式对比

目前三种模式的对比:

  • Cache-aside:由应用管理缓存;未命中时加载,写入时失效。最简单、最常见,可优雅降级,但逻辑分散在应用中,可能出现不一致。
  • Read-through:缓存未命中时自行加载,集中管理读取逻辑。行为与 cache-aside 相同,如果你有支持该模式的缓存层,代码会更整洁。
  • Write-through:缓存同步写入数据库,自动保持缓存与数据库一致,代价是写入延迟和缓存未读数据。

一种常见且实用的做法是:读取仍用 cache-aside,写入时通过一个经过充分测试的共享函数同时更新数据库并失效缓存,获得大部分一致性好处而无需特殊缓存提供者。

一致性从何而来

贯穿这些模式的核心主题是缓存一致性由谁来保证。cache-aside 依赖应用记住去失效缓存。write-through 通过结构强制一致性,因为每次写入都会更新两个存储。如果忘记失效导致缓存陈旧,把模式切换到 write-through(或集中管理失效逻辑)能让一致性自动生效,而非依赖开发者记忆。

read-through 和 write-through 用缓存的自动一致性换取应用的控制权。当你希望缓存逻辑集中且不易遗漏、并能接受 write-through 的延迟时,它们是正确的选择。当你追求简单和优雅降级时,cache-aside 仍是主力。后续模式会更进一步:write-behind 用写入速度换取一致性,refresh-ahead 用预加载避免未命中。

关键要点

  • Read-through 将未命中加载逻辑移到缓存层,集中管理,避免任何代码路径忘记填充缓存。
  • Write-through 让缓存同步写入数据库,自动保持缓存与数据库一致。
  • Write-through 不会加速写入(需要命中两个存储),且无论是否读取都会在写入时缓存数据。
  • Read-through 与 write-through 结合可获得自动缓存一致性,代价是写入延迟和对缓存层的特殊支持。
  • 实用替代方案:cache-aside 读取 + 共享的“更新数据库并失效缓存”函数,在无需特殊基础设施的情况下获得一致性。

常见问题

cache-aside 和 read-through 有什么区别?

cache-aside 由应用在未命中时从数据库加载并填充缓存。read-through 由缓存层自行加载,应用只需从缓存读取。行为相似,但 read-through 集中了逻辑。

什么是 write-through 缓存?

应用写入缓存,缓存在返回前同步写入数据库的模式。两个存储同时更新,保持缓存与数据库一致,代价是更高的写入延迟。

write-through 能让写入更快吗?

不能。写入会同步到达缓存和数据库,速度至少与数据库写入一样慢。write-through 改善的是读取一致性,而不是写入速度;读取受益于缓存始终被填充且最新。

什么时候应该选用 read-through 和 write-through 而非 cache-aside?

当你希望缓存一致性自动强制执行、缓存逻辑集中管理而非分散在应用中,并且能接受额外的写入延迟时。对于简单的缓存场景,cache-aside 通常已经足够。

不使用 write-through 如何保持缓存一致?

使用 cache-aside,但把写入集中在一个共享函数中,该函数同时更新数据库并失效缓存。这样失效操作不易遗漏,能近似 write-through 的一致性,而无需特殊缓存层。

延伸阅读


本文最初发布于 amanksingh.com/blog/redis-read-through-write-through

关于作者

Aman Kumar Singh 是印度诺伊达的团队负责人兼高级软件工程师,专注于使用 TypeScript、Next.js、NestJS、PostgreSQL 和 Redis 进行全栈工程、系统设计和生产级 SaaS 写作。