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 读流程

  1. 应用查询缓存,若命中则直接返回。
  2. 若未命中,查询数据库。
  3. 将查询结果写入缓存,设置过期时间。
  4. 返回数据给调用方。

4.2.2 写流程

  1. 应用更新数据库(先写DB)。
  2. 删除缓存中的对应键(或更新缓存)。
  3. 后续读请求触发缓存重建。
常见陷阱:先删缓存再更新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 实现要点

  1. 使用阻塞队列或消息队列暂存写请求。
  2. 后台线程定期(如每100ms)或按阈值(如1000条)批量刷入DB。
  3. 实现幂等写入,防止重复刷盘导致数据错误。
  4. 配置持久化策略(如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 三种模式流程对比图

下图从应用视角展示三种模式的读写路径差异,帮助理解缓存层与数据库的交互边界。

三种缓存读写模式流程对比 Cache Aside 应用 缓存 数据库 读/写 未命中/写DB 应用直接控制 缓存与DB 先写DB后删缓存 Read/Write Through 应用 缓存代理 数据库 读写 同步代理 应用只与缓存交互 缓存代理DB读写 强一致性 Write Behind 应用 缓存+队列 数据库 写即返回 异步批量 写操作入队即返回 后台线程批量刷DB 最终一致性

4.6 搜索缓存场景选型矩阵

搜索数据类别 推荐模式 理由
商品搜索索引(核心数据) Cache Aside 灵活控制失效,可配合延迟双删保证最终一致性
搜索配置/字典 Read/Write Through 变更少,需要强一致性,缓存代理简化应用逻辑
搜索点击日志 Write Behind 高吞吐写入,可容忍短暂丢失
用户搜索历史 Cache Aside 读写比例高,需要及时更新
实时聚合统计 Write Behind 批量合并写入,降低DB压力
实战经验:在闭环搜索缓存系统中,通常混合使用多种模式。核心索引用Cache Aside保证数据准确性,日志采集用Write Behind保证写入性能,配置元数据用Read/Write Through保证强一致。理解每种模式的权衡,才能设计出高可用、高性能的搜索缓存架构。