很多人以为:
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%)
- 下单 / 支付前确认
- 从 Redis 分布式快照 中读取活动/优惠数据
- 在订单服务内 重新计算金额
- 与 RBM 结果做校验(或仅作为参考)
- 金额在合理误差范围内 → 放行
2️⃣ 兜底路径(0.01%)
以下情况必须 强制回退 DB:
- Redis 快照不存在
- 快照版本不匹配
- 金额偏差超过阈值
- 规则校验失败
👉 DB 结果永远是最终权威结果
七、第五个坑:节点数据不一致,真的只能等“最终一致”?
不是。
我们给系统留了三条退路:
1️⃣ 关键链路兜底校验
-
下单 / 支付前强制查库
-
与 RBM 结果对比
-
异常直接打点
2️⃣ 主动刷新能力
-
通过 XXL-Job
-
支持节点级缓存重建
-
支持维度级重建
3️⃣ MQ Topic 拆分
-
活动变更
-
商品变更
-
黑白名单
避免单 Topic 积压拖垮整个缓存体系。
八、回头看:真正的坑,从来不在 MQ 或 RBM
复盘这套方案,真正重要的不是:
-
用了什么中间件
-
用了什么数据结构
而是这几件事:
- 是否接受缓存天然不一致
- 是否明确缓存的业务边界
- 是否为失败设计兜底
- 是否避免“为了优雅而复杂”
九、写在最后
MQ 同步缓存不是银弹,
它只是把问题从“数据库压力”转移成了“一致性治理”。
真正成熟的系统,不是没有坑,
而是:
每一个坑,都提前准备好了爬出来的梯子。