4、缓存读写策略:Cache Aside、Read/Write Through、Write Behind

好,咱们今天聊聊缓存读写策略。说实话,这是我在实际项目中踩坑最多的一个模块。

你想想看,缓存说白了就是一层加速层。但怎么读、怎么写,策略选不对,轻则数据不一致,重则直接雪崩。我见过太多团队,Redis 用得挺溜,但一问到读写策略,就含糊了。

今天我把三种主流策略掰开揉碎讲清楚。分别是:Cache AsideRead/Write ThroughWrite Behind

4.1 Cache Aside(旁路缓存)

这是最常用的策略。我敢说,90% 的互联网公司都在用这个模式。

它的核心思想很简单:应用程序自己管理缓存。缓存不跟数据库直接打交道,应用层负责读写两边。

读流程

  1. 先查缓存。命中,直接返回。
  2. 没命中,查数据库。
  3. 把数据库结果写入缓存。
  4. 返回数据。

写流程

  1. 更新数据库。
  2. 删除缓存(或者更新缓存)。

嗯,这里要注意。为什么是「删除缓存」而不是「更新缓存」?

我个人习惯是先更新数据库,再删缓存。为什么?因为更新缓存是个「写操作」,如果并发高,多个线程同时更新,很容易出现脏数据。删除缓存就简单了,下次读的时候自然会把新数据拉进来。

核心要点:Cache Aside 模式下,写操作一定要先更新数据库,再操作缓存。顺序不能反。

我的经验:我在做券商行情系统时,就遇到过「先删缓存再更新数据库」的坑。删完缓存后,另一个线程读到了旧数据并写回缓存,导致缓存里一直是旧数据。后来改成「先更新数据库再删缓存」,问题解决。

代码示例

// 读操作
public Order getOrder(String orderId) {
    // 1. 先查缓存
    Order order = redis.get("order:" + orderId);
    if (order != null) {
        return order;
    }
    
    // 2. 缓存没命中,查数据库
    order = orderDao.getById(orderId);
    
    // 3. 写入缓存(设置过期时间)
    if (order != null) {
        redis.set("order:" + orderId, order, 300);
    }
    
    return order;
}

// 写操作
public void updateOrder(Order order) {
    // 1. 先更新数据库
    orderDao.update(order);
    
    // 2. 再删除缓存
    redis.del("order:" + order.getId());
}

4.2 Read/Write Through(读写穿透)

这个策略,说白了就是缓存层自己承担了所有责任。应用层只跟缓存打交道,缓存再跟数据库同步。

我刚开始接触这个模式时觉得挺酷的——应用层代码干净多了。但后来发现,它其实是把复杂度从应用层转移到了缓存层。

读流程

  1. 应用请求缓存。
  2. 缓存命中,直接返回。
  3. 缓存没命中,缓存自己查数据库。
  4. 缓存把结果写入自己,再返回给应用。

写流程

  1. 应用写缓存。
  2. 缓存自己负责写数据库。
  3. 返回成功给应用。

注意:Read/Write Through 需要缓存中间件支持「代理模式」。比如 Redis 本身不支持,需要自己封装一层。我一般用 Guava Cache 或 Caffeine 来实现这种模式,它们内置了 LoadingCache 机制。

代码示例(使用 Caffeine)

LoadingCache<String, Order> orderCache = Caffeine.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(5, TimeUnit.MINUTES)
    .build(key -> {
        // 缓存未命中时,自动从数据库加载
        return orderDao.getById(key);
    });

// 读操作
public Order getOrder(String orderId) {
    return orderCache.get(orderId);
}

// 写操作(需要自己封装)
public void updateOrder(Order order) {
    // 先更新数据库
    orderDao.update(order);
    // 再更新缓存
    orderCache.put(order.getId(), order);
}

你看,读操作确实简洁了。但写操作还是得自己处理。说白了,完全自动化的 Write Through 需要缓存中间件层面支持,比如 Hazelcast 或 Apache Ignite。

4.3 Write Behind(写回/异步写)

这个策略,我愿称之为「性能怪兽」。它的核心思想是:先写缓存,异步批量写数据库

我在券商交易系统中用过这个模式。当时做行情快照的落地,每秒几万笔写入,如果每笔都写数据库,IO 直接打满。用 Write Behind 后,性能提升了 10 倍不止。

写流程

  1. 应用写缓存。
  2. 缓存标记数据为「脏数据」。
  3. 立即返回成功。
  4. 后台线程定期批量刷到数据库。

读流程

  1. 先查缓存。
  2. 命中直接返回。
  3. 没命中,查数据库,写回缓存。

核心优势:写性能极高。适合写多读少、允许短暂数据不一致的场景。

风险提示:如果缓存宕机,未落盘的数据会丢失。我曾经因为这个原因,在凌晨被电话叫醒过……所以,一定要配合持久化机制(比如 Redis AOF/RDB)或者 WAL 日志来保证数据安全。

代码示例(简化版)

// 使用一个队列暂存待写入的数据
private final BlockingQueue<Order> writeQueue = new LinkedBlockingQueue<>();

// 写操作
public void updateOrder(Order order) {
    // 1. 先更新缓存
    redis.set("order:" + order.getId(), order);
    
    // 2. 放入写队列
    writeQueue.offer(order);
}

// 后台线程批量刷库
@Scheduled(fixedDelay = 1000) // 每秒刷一次
public void flushToDatabase() {
    List<Order> batch = new ArrayList<>();
    writeQueue.drainTo(batch, 100); // 每次最多取100条
    
    if (!batch.isEmpty()) {
        orderDao.batchUpdate(batch);
    }
}

三种策略对比

策略 读性能 写性能 数据一致性 实现复杂度 适用场景
Cache Aside 强(最终一致) 通用场景,大部分业务系统
Read/Write Through 需要缓存层统一管理的场景
Write Behind 极高 弱(最终一致) 写密集型、日志、统计、行情快照

知识体系结构图

缓存读写策略知识体系 缓存读写策略 Cache Aside Read/Write Through Write Behind 先更新DB 再删缓存 缓存代理 自动同步 异步批量 最终一致 选择策略 = 权衡性能、一致性、复杂度

如何选择?

说实话,没有银弹。我一般这样选:

  • 通用业务系统:Cache Aside 就够了。简单、可靠、容易排查问题。
  • 需要缓存层统一管理:比如微服务架构中,多个服务共享同一套缓存逻辑,用 Read/Write Through 封装一层。
  • 写密集型场景:比如日志、统计、行情快照,用 Write Behind。但一定要做好容灾。

我的建议:刚开始做缓存设计时,先从 Cache Aside 入手。等你对业务场景和数据一致性要求有了深刻理解后,再考虑其他策略。别一上来就整 Write Behind,容易翻车。

嗯,这三种策略就讲到这里。每种都有它的脾气,选对了事半功倍,选错了……嗯,你懂的。