Cache-Aside + 延迟双删 + 异步重试 + Binlog监听

1. 核心组件与协作流程

1.1 整体架构
+-----------------+       +----------------+       +-----------------+       +-----------------+
|   Application   |       |      Redis     |       |     MySQL       |       |   MQ/Binlog     |
| (Cache-Aside)   |<----->| (缓存层)        |<----->| (数据库)         |<----->| (Canal/Kafka)   |
+-----------------+       +----------------+       +-----------------+       +-----------------+
      | 读/写请求               | 缓存操作               | 数据持久化               | 监听数据变更
      |                         |                       |                         |
      v                         v                       v                         v
  读流程:优先查缓存         延迟双删策略              数据更新操作             异步更新/删除缓存
  写流程:先DB后缓存         异步重试机制              Binlog日志              消息队列保证可靠性

2. 分步骤实现原理

2.1 写操作流程(应用主动更新)
  1. 第一步:删除缓存(第一次删除)

    • 应用在更新数据库前,先删除Redis中的缓存数据

    • 目的:防止后续并发读请求命中旧缓存。

  2. 第二步:更新数据库

    • 执行MySQL的更新操作(如UPDATE语句)。

  3. 第三步:延迟双删(第二次删除)

    • 在数据库更新完成后,延迟一段时间(如500ms),再次删除缓存。

    • 原理

      • 延迟时间需覆盖“其他线程可能读取旧数据并回填缓存”的时间窗口。

      • 例如:线程A更新DB后,线程B可能在更新期间读取旧数据并写入缓存,延迟双删可清除残留旧数据。

  4. 异步重试机制

    • 若第一次或第二次删除缓存失败,将删除操作提交到消息队列(如Kafka),由消费者异步重试,直至成功。

    • 关键点

      • 消息队列需支持至少一次投递(保证可靠性)。

      • 消费者需实现幂等性(多次删除同一缓存无副作用)。

2.2 读操作流程(Cache-Aside)
  1. 读取缓存命中,直接返回数据。

  2. 缓存未命中时,查询数据库并回填缓存。

    • 回填时加分布式锁:防止多个线程并发回填相同数据(如用Redis的SETNX命令)。

2.3 Binlog监听(兜底一致性)
  1. 监听数据库变更

    • 使用CanalDebezium监听MySQL的Binlog日志,捕获所有数据变更事件(增删改)。

  2. 发布变更事件到消息队列

    • 将变更事件(如表名、主键ID、操作类型)发送至Kafka,保证事件不丢失。

  3. 消费者处理事件

    • 消费者根据事件类型(如UPDATE删除或更新对应的缓存

    • 示例逻辑

      // 消费者处理Binlog事件
      public void handleEvent(DataChangeEvent event) {
          if (event.getTable().equals("user")) {
              String cacheKey = "user:" + event.getId();
              redis.delete(cacheKey); // 直接删除缓存,后续读请求触发回填
          }
      }
  4. 意义

    • 作为“延迟双删”的兜底机制,防止因应用层双删失败或网络抖动导致缓存未清理。

    • 确保即使应用层逻辑异常,Binlog监听仍能最终清理旧缓存。


3. 关键问题与解决方案

3.1 延迟双删的时间如何确定?
  • 经验值:通常设置为业务读取耗时的2-3倍(如500ms)。

  • 动态调整:根据实际业务监控的缓存回填时间动态优化。

3.2 如何避免缓存回填旧数据?
  • 回填加锁

    // 伪代码:回填缓存时加分布式锁
    String lockKey = "lock:user:" + id;
    if (redis.setnx(lockKey, "1", 10s)) {
        try {
            // 查询数据库并写入缓存
        } finally {
            redis.delete(lockKey);
        }
    }
  • 版本号校验
    缓存数据携带版本号(或时间戳),写入时校验版本,防止旧数据覆盖新数据。

3.3 消息队列的可靠性保障
  • 生产者端:确保删除操作事件100%投递到MQ(如Kafka的ACK机制)。

  • 消费者端

    • 至少一次消费(避免消息丢失)。

    • 消费失败时重试,并记录死信队列(防止阻塞其他消息)。

3.4 缓存与数据库操作原子性问题
  • 最终一致性原则:接受短暂不一致,通过异步机制最终达成一致。

  • 无法完全避免的场景

    • 若强一致性要求极高(如金融交易),需牺牲性能,采用同步写(如Write-Through)+ 分布式事务。


4. 优缺点分析

优点缺点
1. 最终一致性保障,容错能力强。1. 架构复杂,需维护消息队列和Binlog监听。
2. 高并发下性能优异,避免强一致的开销。2. 延迟双删的时间设置依赖经验值。
3. 通过多级保障(双删+Binlog)降低不一致概率。3. 短暂不一致窗口无法完全消除。

5. 适用场景

  • 读多写少:如电商商品详情页、社交动态信息。

  • 容忍最终一致性:非金融、支付类业务。

  • 高并发场景:需要平衡性能与一致性的系统。


6. 总结

该组合模式通过以下四层保障实现高效一致性:

  1. Cache-Aside:基础读写分离策略,避免缓存穿透。

  2. 延迟双删:主动清理可能的旧缓存,降低不一致窗口。

  3. 异步重试:保证删除操作最终成功。

  4. Binlog监听:兜底清理残留旧缓存,实现最终一致性。

核心思想

  • 优先删除缓存而非更新,避免并发写导致数据混乱。

  • 异步化与解耦:通过消息队列和Binlog监听,将缓存清理操作与主流程解耦,提升系统健壮性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

何怀逸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值