题目
如何设计一个基于最终一致性的跨服务订单支付系统?
信息
- 类型:问答
- 难度:⭐⭐
考点
分布式事务,最终一致性,消息队列,幂等性
快速回答
实现最终一致性的核心方案:
- 使用消息队列解耦订单服务和支付服务
- 通过本地事务表保证本地操作与消息发送的原子性
- 支付服务实现幂等操作处理重复消息
- 设置补偿机制处理失败场景(如定时对账)
- 采用异步通知更新订单状态
1. 问题场景
在电商系统中,用户下单后需要调用支付服务完成扣款。要求:
1) 订单服务创建订单
2) 支付服务执行扣款
3) 两服务数据库独立
需保证数据最终一致,避免支付成功但订单状态未更新等异常。
2. 最终一致性方案设计
核心流程:
// 订单服务伪代码
@Transactional
void createOrder(Order order) {
// 1. 订单数据写入本地数据库
orderDao.insert(order);
// 2. 事务消息写入本地消息表(与订单同库同事务)
Message msg = new Message("PAY_REQUEST", order.getId());
messageDao.insert(msg);
}
// 3. 独立线程轮询消息表并投递到MQ
void sendToMQ(Message msg) {
if (mq.send(msg)) {
messageDao.markAsSent(msg.id); // 标记已发送
}
}
// 支付服务消费
@RabbitListener(queues = "PAY_QUEUE")
void handlePayment(Message msg) {
// 4. 检查幂等性(防止重复消费)
if (paymentService.isProcessed(msg.id)) return;
// 5. 执行支付业务
paymentService.process(msg.orderId);
// 6. 发送支付结果通知
mq.send(new Message("PAY_RESULT", orderId, "SUCCESS"));
}关键组件说明:
| 组件 | 作用 | 实现要点 |
|---|---|---|
| 本地消息表 | 保证本地事务与消息发送原子性 | 与业务数据同库,事务提交时同步写入 |
| 消息队列 | 服务间解耦与异步通信 | Kafka/RabbitMQ/RocketMQ(需支持持久化) |
| 幂等设计 | 防止消息重复消费 | 在支付服务端使用唯一ID+状态机校验 |
| 补偿机制 | 处理失败场景 | 定时任务扫描超时订单主动查询支付状态 |
3. 最佳实践
- 消息可靠性:MQ需配置生产者确认+消费者ACK机制
- 超时控制:支付操作设置超时时间(如30秒)
- 状态机设计:
订单状态:CREATED → PAYING → PAID/FAILED - 对账系统:每日核对订单与支付系统的状态差异
4. 常见错误
- ❌ 直接使用MQ事务消息(性能差)→ 改用本地消息表
- ❌ 忽略幂等设计 → 导致重复扣款
- ❌ 未设置超时机制 → 用户长时间等待
- ❌ 补偿任务频率过高 → 增加系统负载
5. 扩展知识
- Saga模式:长事务解决方案,通过反向操作回滚
- TCC模式:Try-Confirm-Cancel三阶段控制
- 对比2PC:最终一致性 vs 强一致性(CAP中放弃C保证AP)
- 监控指标:消息积压量、事务完成延迟、补偿触发次数