直读模式(Read Through Pattern) 是一种将缓存作为“主要数据源”的架构模式。与 旁路缓存 (Cache Aside) 不同,在 Read Through 模式下,应用程序不再直接与数据库交互,而是通过一个缓存代理(或特殊的缓存库)来获取数据。
其核心思想是:应用只看缓存,缓存负责加载数据。
核心操作流程
一、读操作 (Read Path)
场景 1:缓存命中 (Cache Hit)
sequenceDiagram participant App as 应用程序 participant Cache as 缓存 App->>Cache: 1. 查询数据 Cache-->>App: 2. 返回命中数据
场景 2:缓存失效 (Cache Miss)
sequenceDiagram participant App as 应用程序 participant Cache as 缓存 participant DB as 数据库 App->>Cache: 1. 查询数据 Note over Cache, DB: 缓存层自动触发回填 Cache->>DB: 2. 读取数据库 DB-->>Cache: 3. 返回原始数据 Cache->>Cache: 4. 写入缓存 Cache-->>App: 5. 返回数据给应用
- 请求数据:应用程序向缓存层请求数据。
- 命中返回:如果缓存中有数据,直接返回。
- 自动加载:如果缓存缺失(Cache Miss),由缓存层自动去数据库读取数据。
- 回填缓存:缓存层将从数据库读取到的数据写入自身。
- 返回结果:缓存层将数据返回给应用程序。
对于应用层代码来说,读取缓存的操作是透明的:它就像是在读取一个超大、高性能的数据库,而不需要关心数据是来自内存还是磁盘。
二、写操作 (配套模式)
从严格定义上讲,Read Through 仅代表一种读策略。但在实际应用中,由于 Read Through 的前提是“应用只与缓存交互”,因此其对应的写操作通常由以下两种配套模式完成:
- 直写模式 (Write Through):写操作同步经过缓存层并写入数据库。
- 回写模式 (Write Back):写操作只写入缓存后立即返回,由缓存层异步批量刷入数据库。
这种“读写全交给缓存”的组合架构,也被合称为透明缓存(In-Line Cache)。
关键考量:并发保护与加载策略
一、请求合并 (Request Collapsing)
在 Cache Aside 模式下,如果多个线程同时发现缓存失效,可能会同时去查询数据库,导致缓存击穿。
而优秀的 Read Through 实现(如 Caffeine, Guava Cache)通常内置了请求合并机制:
- 当一个 Key 发生 Cache Miss 时,第一个到达的线程会触发“加载”动作。
- 其他同时请求该 Key 的线程会被阻塞,进入等待队列。
- 加载线程完成后,结果会同步给所有等待的线程。
这种机制从底层保证了同一个 Key 无论并发多高,只会产生一次数据库查询,极大地保护了后端数据库。
二、进阶加载策略
根据业务对延迟和一致性的要求,Read Through 可以有不同的执行策略:
- 同步加载(默认):Cache Miss 时,请求线程阻塞等待 DB 返回。适用于对一致性要求高的场景。
- 异步加载(Async):缓存返回
Future或CompletableFuture,应用线程不原地阻塞。 - 刷新提前(Refresh Ahead):在数据过期前,缓存层自动异步刷新。优点是零延迟,缺点是可能读到短暂的旧数据。
三、一致性挑战:数据新鲜度
虽然 Read Through 简化了业务逻辑,但它在一致性上存在两个潜在挑战:
- 脏数据回填风险:如果存在外部系统或后台任务直接绕过缓存修改数据库,
Read Through可能会在回填时加载到旧值并长期驻留。因此,该模式必须配合 TTL (过期时间) 作为最终一致性的保底手段。 - 闭环完整性:该模式最理想的状态是与 Write Through 配合形成闭环。如果写操作是零散的(如 Cache Aside 式的先更库再删缓存),那么在删缓存和 Read Through 重新回填的极短间隙内,依然存在并发导致的数据不一致风险。
适用场景
Read Through 模式通常出现在以下场景中:
- 进程内缓存库:如 Java 的 Guava Cache 或 Caffeine。你只需配置一个
CacheLoader,查询时如果 Key 不存在,库会自动调用该 Loader。 - 数据库中间件/分布式系统:某些高端的分布式缓存系统支持配置外部数据源。
- 只读业务:对于那些几乎不更新,或者更新频率极低且对实时性要求不高的热点配置数据。
总结
- 优点:业务逻辑解耦,代码极其简洁;底层内置并发控制,天然防止缓存击穿。
- 缺点:实现成本较高,通常需要缓存库或中间件支持;数据模型灵活性较差。
- 适用场景:本地进程内缓存(Caffeine, Guava);对读性能和一致性有严格要求的透明代理场景。