4. 缓存读写模式:Cache Aside、Read/Write Through、Write Behind Caching
在构建高并发搜索缓存系统时,选择正确的缓存读写模式是保证数据一致性与系统性能的核心决策。本章深入剖析三种主流模式:Cache Aside(旁路缓存)、Read/Write Through(读写穿透)和Write Behind Caching(写回缓存),并结合搜索场景分析其适用边界。
4.1 三种模式核心对比
| 维度 | Cache Aside | Read/Write Through | Write Behind Caching |
|---|---|---|---|
| 读路径 | 应用先查缓存,未命中则查DB并回写缓存 | 应用只与缓存层交互,缓存负责从DB加载 | 同Read Through,缓存层代理读操作 |
| 写路径 | 应用先更新DB,再删除或更新缓存 | 应用写入缓存,缓存同步写入DB | 应用写入缓存即返回,缓存异步批量写入DB |
| 一致性保障 | 最终一致性,需处理并发写冲突 | 强一致性(缓存与DB同步写) | 弱一致性,存在数据丢失窗口 |
| 写延迟 | 低(异步删除缓存) | 中(同步写DB) | 极低(仅写缓存) |
| 实现复杂度 | 低,应用层控制 | 中,需封装缓存代理层 | 高,需队列、重试、幂等机制 |
| 搜索场景典型用例 | 商品搜索索引、用户画像缓存 | 配置中心、字典数据 | 点击流、搜索日志、聚合统计 |
选型建议:搜索缓存场景中,Cache Aside 是最广泛采用的模式,因为它将缓存与数据库解耦,允许灵活控制失效策略。Write Behind 仅适用于对一致性要求不高的日志型数据。
4.2 Cache Aside(旁路缓存模式)
Cache Aside 是搜索系统中最常见的缓存模式。应用层同时感知缓存和数据库,读写逻辑由业务代码显式控制。
4.2.1 读流程
- 应用查询缓存,若命中则直接返回。
- 若未命中,查询数据库。
- 将查询结果写入缓存,设置过期时间。
- 返回数据给调用方。
4.2.2 写流程
- 应用更新数据库(先写DB)。
- 删除缓存中的对应键(或更新缓存)。
- 后续读请求触发缓存重建。
常见陷阱:先删缓存再更新DB会导致并发读请求读到旧数据。正确做法是先更新DB,后删缓存,配合延迟双删策略(先删缓存→更新DB→延迟再删缓存)可进一步降低不一致窗口。
// 伪代码:Cache Aside 写操作
public void updateSearchIndex(String key, SearchDocument doc) {
// 1. 更新数据库
database.update(key, doc);
// 2. 删除缓存,触发下次读时重建
cache.del(key);
// 3. 可选:延迟双删(1秒后再次删除)
executor.schedule(() -> cache.del(key), 1, TimeUnit.SECONDS);
}
4.3 Read/Write Through(读写穿透模式)
Read/Write Through 将缓存层封装为数据源代理,应用只与缓存层交互,缓存层负责与数据库同步。这种模式在搜索系统中常用于配置缓存和字典数据,因为这些数据变更频率低且对一致性要求高。
4.3.1 读流程
- 应用调用缓存层读取接口。
- 缓存层检查本地缓存,命中则返回。
- 未命中则缓存层查询数据库,填充缓存后返回。
4.3.2 写流程
- 应用调用缓存层写入接口。
- 缓存层先更新本地缓存。
- 缓存层同步写入数据库。
- 返回写入结果给应用。
// 伪代码:Write Through 缓存代理
public class SearchCacheProxy {
public SearchDocument read(String key) {
SearchDocument doc = cache.get(key);
if (doc == null) {
doc = database.load(key);
cache.put(key, doc);
}
return doc;
}
public void write(String key, SearchDocument doc) {
cache.put(key, doc); // 先更新缓存
database.update(key, doc); // 同步写DB
}
}
搜索场景适配:当搜索索引的元数据(如分词词典、排序权重配置)需要强一致性时,Read/Write Through 能保证缓存与数据库始终同步,避免因缓存不一致导致搜索结果异常。
4.4 Write Behind Caching(写回缓存模式)
Write Behind Caching 将写操作全部在缓存层完成,然后异步批量写入数据库。这种模式在搜索系统中常用于高吞吐写入场景,如搜索点击日志、用户行为采集、实时聚合统计。
4.4.1 核心特征
- 写性能极高:只写内存,不阻塞DB写入。
- 异步批量:缓存层合并多次写操作,批量刷入DB。
- 数据丢失风险:缓存宕机时未刷盘的写操作会丢失。
- 最终一致性:DB数据落后于缓存,但最终会追上。
4.4.2 实现要点
- 使用阻塞队列或消息队列暂存写请求。
- 后台线程定期(如每100ms)或按阈值(如1000条)批量刷入DB。
- 实现幂等写入,防止重复刷盘导致数据错误。
- 配置持久化策略(如Redis AOF/RDB)降低丢失风险。
// 伪代码:Write Behind 批量刷盘
public class WriteBehindCache {
private final BlockingQueue<WriteRequest> queue = new LinkedBlockingQueue<>();
public void write(String key, SearchDocument doc) {
queue.offer(new WriteRequest(key, doc)); // 入队即返回
}
@Scheduled(fixedDelay = 100) // 每100ms刷一次
public void flush() {
List<WriteRequest> batch = new ArrayList<>();
queue.drainTo(batch, 1000); // 最多取1000条
if (!batch.isEmpty()) {
database.batchUpdate(batch); // 批量写DB
}
}
}
搜索场景风险:如果使用Write Behind缓存搜索索引的增量数据,一旦缓存宕机,未刷盘的文档更新将永久丢失,导致搜索结果与数据库不一致。建议仅用于可容忍丢失的辅助数据,如搜索热词统计、点击率预计算。
4.5 三种模式流程对比图
下图从应用视角展示三种模式的读写路径差异,帮助理解缓存层与数据库的交互边界。
4.6 搜索缓存场景选型矩阵
| 搜索数据类别 | 推荐模式 | 理由 |
|---|---|---|
| 商品搜索索引(核心数据) | Cache Aside | 灵活控制失效,可配合延迟双删保证最终一致性 |
| 搜索配置/字典 | Read/Write Through | 变更少,需要强一致性,缓存代理简化应用逻辑 |
| 搜索点击日志 | Write Behind | 高吞吐写入,可容忍短暂丢失 |
| 用户搜索历史 | Cache Aside | 读写比例高,需要及时更新 |
| 实时聚合统计 | Write Behind | 批量合并写入,降低DB压力 |
实战经验:在闭环搜索缓存系统中,通常混合使用多种模式。核心索引用Cache Aside保证数据准确性,日志采集用Write Behind保证写入性能,配置元数据用Read/Write Through保证强一致。理解每种模式的权衡,才能设计出高可用、高性能的搜索缓存架构。