MQ 同步更新缓存:看似简单,其实坑很多

23 阅读5分钟

很多人以为:

MQ 广播 + 本地缓存 = 高性能 + 高可用

真正落地之后才发现:

同步的不是数据,是不一致本身。

这篇文章不讨论“该不该用 MQ”,

而是结合一次真实业务实践,聊聊:

  • MQ 同步本地缓存到底会在哪些地方失效
  • 为什么这些问题不是 bug,而是设计必然
  • 又该如何在工程上把不可避免的问题关进笼子里

一、背景:为什么非要走 MQ + 本地缓存?

业务背景并不特殊:

  • 商品规模:几十万

  • 活动规模:万级

  • 高频场景:

    • 商品列表打活动标
    • 加购页到手价计算
  • 特点:

    • 读极高频

    • 写相对低频

如果每次请求都查库:

  • RT 不稳定

  • DB 成为瓶颈

于是,MQ 广播同步本地缓存成了看起来“理所当然”的选择。

但问题从这一刻才真正开始。


二、第一个坑:MQ 广播,本地数据一定一致吗?

不会,一定不会。

哪怕你用的是 RocketMQ 广播模式,也无法保证:

  • 消息严格顺序

  • 所有节点同时消费

  • 节点永不宕机

  • 消费永不失败

所以第一个必须接受的事实是:

MQ 同步缓存,从来就不是强一致方案。

如果你在设计之初假设「所有节点数据一定一致」,

那后面所有设计都会是错的。


三、第二个坑:缓存结构选错,后面全是灾难

我们一开始用的是:

  • ConcurrentHashMap

  • Set / List

  • 多重循环匹配规则

问题很快暴露:

  • CPU 消耗巨大

  • 内存占用高

  • 高并发下 RT 抖动明显

本质原因是:

这是一个“集合交集”问题,却用循环硬算。

这一步认知转变之后,才引入了 RoaringBitmap(RBM)


四、第三个坑:RBM 更新方式,网上说的未必适合你

经典建议:全量重建 + 原子替换

这是很多文章里推荐的“标准姿势”。

但在真实业务中,这个方案往往被高估了

为什么?

以「区域 → 活动 RBM」为例:

  • 一个区域最多 1 万多个活动

  • 一次变更,可能只影响 1~2 个活动

如果每次 MQ 都:

  • rebuild 整个 Bitmap

  • 分配新对象

  • 触发 GC

那么结局只有一个:

CPU 峰值 + 内存抖动


五、我们的选择:接受不完美,而不是追求“教科书方案”

1️⃣ 消费模型先定死

  • 单 Consumer

  • 单线程顺序消费

  • 不追求并发写

这一步非常关键,它直接决定了后面的更新策略。


2️⃣ RBM 更新策略:局部写,而不是全量重构

  • 新增活动 → bitmap.add(id)

  • 下线活动 → bitmap.remove(id)

由于:

  • 写入低频

  • 串行消费

  • 更新粒度小

👉 不需要复杂锁,也不需要 Copy-On-Write

这是一个工程现实主义的选择。


六、第四个坑:你以为 RBM 能贯穿整个交易链路?

不能,也不应该。

RBM 适合的地方

  • 商品列表打标

  • 加购页到手价预估

特点:

  • 高频
  • 可容忍短时间不一致
  • 用户体验优先

RBM 不该参与的地方

  • 下单提交

  • 支付前金额确认

这些节点:

  • 必须查库
  • 必须重新计算
  • RBM 只能作为参考

说到这里,很多人会有一个现实疑问:

大型活动 / 大促场景
最终订单结算并发极高
优惠计算逻辑又非常复杂
全部实时查 DB + 重算,DB 扛不住怎么办?

这是一个非常现实的问题

工程上的折中方案:牺牲 0.01% 准确性,换取系统稳定

在工程实践中,我们可以做一个明确的取舍

在保证绝对安全兜底的前提下,允许 99.99% 的准确性。

核心思路

  • ❌ 不使用 RBM 作为最终结果
  • ✅ 使用 持久化 Redis 分布式缓存 作为“准权威数据源”
  • ✅ 所有服务实例 统一读取中心缓存
  • ❌ 禁止依赖各服务器的本地缓存(防止不一致)

最终下单链路

1️⃣ 正常路径(99.99%)

  1. 下单 / 支付前确认
  2. Redis 分布式快照 中读取活动/优惠数据
  3. 在订单服务内 重新计算金额
  4. 与 RBM 结果做校验(或仅作为参考)
  5. 金额在合理误差范围内 → 放行

2️⃣ 兜底路径(0.01%)

以下情况必须 强制回退 DB

  • Redis 快照不存在
  • 快照版本不匹配
  • 金额偏差超过阈值
  • 规则校验失败

👉 DB 结果永远是最终权威结果


七、第五个坑:节点数据不一致,真的只能等“最终一致”?

不是。

我们给系统留了三条退路:

1️⃣ 关键链路兜底校验

  • 下单 / 支付前强制查库

  • 与 RBM 结果对比

  • 异常直接打点

2️⃣ 主动刷新能力

  • 通过 XXL-Job

  • 支持节点级缓存重建

  • 支持维度级重建

3️⃣ MQ Topic 拆分

  • 活动变更

  • 商品变更

  • 黑白名单

避免单 Topic 积压拖垮整个缓存体系。


八、回头看:真正的坑,从来不在 MQ 或 RBM

复盘这套方案,真正重要的不是:

  • 用了什么中间件

  • 用了什么数据结构

而是这几件事:

  1. 是否接受缓存天然不一致
  2. 是否明确缓存的业务边界
  3. 是否为失败设计兜底
  4. 是否避免“为了优雅而复杂”

九、写在最后

MQ 同步缓存不是银弹,

它只是把问题从“数据库压力”转移成了“一致性治理”。

真正成熟的系统,不是没有坑,

而是:

每一个坑,都提前准备好了爬出来的梯子。