分布式事务 - 基于消息传递的分布式事务一致性的实现
场景
以电商场景为例子。
场景1:下单: ①生成订单 -- 本地事务 ②占销售库存 --远程微服务调用
场景2:订单评审通过: ①SSO推结算,②SSO推供应链。
场景3:金融场景。
场景1,2个子事务只要有一个失败,就回滚。
场景2:2个子事务,不管是否失败,都继续补偿重试,如果重试几次还是失败,人工介入。
如果不用成熟的框架(Seata),该如何实现?
本地消息表是一种基于可靠消息最终一致性的分布式事务解决方案,通过将消息持久化与业务操作绑定在同一个本地事务中,确保跨服务操作的最终一致性。以下是其核心实现原理、流程及优化策略:
四、适用场景
- 最终一致性业务:
✅ 订单支付后通知发货
✅ 用户注册送积分
✅ 主库数据变更同步搜索索引。 - 不适用场景:
❌ 强一致性要求(如金融转账) ,下单成功占销售库存。
❌ 高频事务(消息表写入压力大)。
一、核心原理
-
事务原子性保证
在业务操作(如创建订单)的同一数据库事务中,插入一条消息到本地消息表,利用数据库的ACID特性确保业务数据与消息记录同时成功或回滚。- 表结构示例:
CREATE TABLE local_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_id VARCHAR(64) NOT NULL, -- 业务ID(如订单ID) content TEXT NOT NULL, -- 消息内容(JSON格式) status TINYINT NOT NULL DEFAULT 0, -- 状态(0-待发送, 1-已发送, 2-已确认, 3-死亡) retry_count INT DEFAULT 0, -- 重试次数 created_time DATETIME NOT NULL );
- 关键索引:对
status
和created_time
建立联合索引,加速扫描效率。
- 表结构示例:
-
异步消息投递
通过定时任务或独立线程扫描状态为待发送(0)
的消息,发送至消息队列(如RabbitMQ/Kafka),成功后更新状态为已发送(1)
;失败则增加重试次数,超阈值后标记为死亡(3)
需人工介入。 -
消费者幂等性
消费者需实现幂等逻辑(如唯一业务ID校验),避免消息重复消费导致数据不一致。@KafkaListener(topics = "order_created") public void consume(Message msg) { if (deductLogService.exists(msg.getBizId())) return; // 幂等检查 inventoryService.deductStock(msg.getContent()); // 业务逻辑 deductLogService.logSuccess(msg.getBizId()); // 记录消费 }
-
补偿机制
- 发送失败:定时任务重新扫描
待发送
消息重试投递。 - 消费失败:MQ重试投递,消费者返回NACK;若持续失败则进入死信队列。
- 状态不一致:定时对账任务检查
已发送但未确认
的消息,重新投递或修复状态。
- 发送失败:定时任务重新扫描
二、工作流程
sequenceDiagram
participant A as 事务发起方
participant DB as 业务数据库
participant MQ as 消息队列
participant B as 消费者
A->>DB: 1. 开启事务
A->>DB: 2. 更新业务数据 + 插入消息记录
A->>DB: 3. 提交事务(原子性保证)
loop 定时任务
A->>DB: 4. 扫描待发送消息
A->>MQ: 5. 发送消息
MQ-->>A: 6. 发送成功回调
A->>DB: 7. 更新消息状态为“已发送”
end
MQ->>B: 8. 推送消息
B->>B: 9. 幂等校验
B->>B: 10. 执行业务逻辑
B->>MQ: 11. 返回ACK
三、优缺点分析
优点 | 缺点 | 优化策略 |
---|---|---|
1. 实现简单,无需额外中间件(如Seata) | 1. 消息延迟(依赖定时扫描,秒~分钟级) | 分库分表、批量发送、MQ延迟消息 |
2. 高可靠性(本地事务保消息必达) | 2. 数据库压力大(消息表与业务库耦合) | 消息表独立部署、读写分离 |
3. 业务侵入低(仅需新增消息表) | 3. 需消费者实现幂等性 | 唯一键+状态机、消费日志表 |