腾讯二面:如何保证MQ消息不丢失?重复消费如何保证幂等,本地消息表配合MQ实现最终一致性?
收银台系统如何实现跨库分布式事务?——基于本地消息表与 MQ 的最终一致性实践
在金融级系统中,数据一致性是生命线。
我们在收银台系统中,通过「本地消息表 + 消息队列」的组合,在不依赖 Seata 等中间件的前提下,实现了跨基本库与交易库的可靠分布式事务,并完成了服务间的异步解耦。
一、业务背景:收银台的跨库挑战
我们的收银台系统涉及两类核心数据:
| 数据库 | 职责 | 示例 |
|---|---|---|
| 基本库(Basic DB) | 存储用户身份、会员等级、可用权益(如优惠券、积分、免费额度) | user_benefits 表 |
| 交易库(Transaction DB) | 存储订单、支付记录、账单等交易流水 | orders, payments 表 |
典型场景:用户使用“会员免费额度”下单
- 用户发起支付请求
- 系统从 基本库 扣减其免费额度(权益)
- 同时在 交易库 创建订单和支付记录
- 返回支付结果
这两个操作跨两个物理数据库,无法用一个本地事务保证原子性。如果只扣了权益但没建订单,或只建了订单但没扣权益,都会导致资损或用户体验问题。
二、为什么不用 Seata?
虽然 Seata 提供了 AT、TCC 等模式,但在我们评估后,发现:
- 引入 Seata 会增加运维复杂度(需部署 TC、维护 undo_log 等)
- 对数据库有侵入(如需要全局锁、undo 表)
- 在高并发支付场景下,性能损耗不可忽视
- 团队更倾向于 轻量、可控、可审计 的方案
因此,我们选择了 更经典、更透明的「本地消息表 + MQ」方案。
三、解决方案:本地消息表 + MQ 实现最终一致性
核心思路
将跨库操作拆解为:本地事务 + 异步消息通知,通过“先写消息再发 MQ”的方式,确保关键动作不丢失。
具体流程(以“扣权益 → 创建订单”为例)
步骤 1:在基本库中执行本地事务
-- 基本库(Basic DB)
BEGIN;-- 1. 扣减用户权益(如免费额度)
UPDATE user_benefits SET free_quota = free_quota - 100
WHERE user_id = 123 AND free_quota >= 100;-- 2. 插入本地消息表(同一事务!)
INSERT INTO local_outbox (msg_id, business_type, payload, status, create_time
) VALUES ('msg_20251116_001','CREATE_ORDER','{"userId":123, "amount":100, "benefitUsed":true}','pending',NOW()
);COMMIT;
✅ 此时:权益已扣 + 消息已落库,两者强一致。
步骤 2:异步任务投递消息到 MQ
- 后台补偿服务定时扫描
local_outbox中status = 'pending'的记录 - 调用 RocketMQ/Kafka 发送消息
- 发送成功 → 更新状态为
sent - 失败 → 重试(带退避策略)
步骤 3:交易库消费消息,创建订单
- 订单服务监听 MQ
- 收到消息后,在 交易库 中创建订单和支付记录
- 必须做幂等处理(如通过
msg_id或业务唯一键去重)
四、本地消息表带来的三大价值
✅ 1. 保证跨库操作的最终一致性
- 即使服务宕机、MQ 不可用,消息也不会丢失
- 重启后自动补偿,确保“权益扣了,订单一定建”
✅ 2. 实现真正的异步解耦
- 基本库服务(权益中心)不直接调用订单服务
- 两者通过 MQ 通信,独立演进、独立部署
- 订单服务故障时,消息可堆积,不影响权益扣减主流程
✅ 3. 规避分布式事务中间件的复杂性
- 无需引入 Seata、XA、2PC 等重型方案
- 所有逻辑透明可控,便于审计、排查、回滚
五、关键设计细节
| 项目 | 实践建议 |
|---|---|
| 消息表命名 | 建议叫 local_outbox(业界通用术语) |
| 消息内容 | 冗余关键业务字段,避免后续查表失败 |
| 幂等性 | 消费端用 msg_id 做唯一索引,防止重复创建订单 |
| 重试机制 | 最多重试 N 次(如 5 次),失败后进入死信队列人工处理 |
| 监控告警 | 监控 pending 消息积压量、发送成功率 |
| 清理策略 | 成功消息保留 7~30 天后归档,避免表过大 |
六、为什么不直接发 MQ?
你可能会问:为什么不在扣权益后直接 syncSend 消息?
答案是:无法保证原子性。
- 如果先发 MQ 再 commit:MQ 成功但 commit 失败 → 白送权益
- 如果先 commit 再发 MQ:commit 成功但发 MQ 前宕机 → 权益扣了但订单没建
只有把“发消息”变成“事务的一部分”(即先写本地消息表),才能从根本上解决这个问题。
技术没有银弹,但有最适合业务的方案。
我们的实践证明:简单、透明、可靠的架构,往往走得更远。
腾讯二面:如何保证MQ消息不丢失?重复消费如何保证幂等?-腾讯云开发者社区-腾讯云

