实战支付系统:分布式事务在考试中的标准回答模板
一、 问题背景(考试答题第一步)
场景描述:
在典型的支付业务中,我们需要同时操作两个独立的系统:本地交易数据库(记录订单状态)和外部支付网关(如支付宝、微信支付)或库存服务。
核心矛盾:
由于微服务架构下数据库被拆分,传统的 ACID 事务(本地数据库事务)无法跨库生效。如果本地订单扣款成功,但调用外部支付网关失败,或者库存服务回滚失败,就会导致数据不一致(如:钱扣了,订单却未成功)。为了解决此问题,必须引入分布式事务解决方案。
二、 解决方案核心策略(答题得分的“骨架”)
在支付场景中,由于涉及外部系统(银行/第三方),我们通常无法锁定其资源,因此 2PC(两阶段提交) 和 3PC(三阶段提交) 因其阻塞性强、性能差,很少直接用于支付链路。
标准答案是:
- 对于内部服务间的一致性(如订单-库存): 推荐使用 TCC (Try-Confirm-Cancel) 或 Seata 的 AT 模式。
- 对于涉及外部支付的一致性(如订单-支付网关): 推荐使用 基于消息队列的最终一致性方案(可靠消息最终一致性) 。
三、 核心方案一:TCC (Try-Confirm-Cancel) 实战
TCC 将业务逻辑拆分为三个阶段,应用层控制,性能高但代码侵入性强。
- Try 阶段:资源的检查和预留(如冻结资金、预扣库存)。
- Confirm 阶段:确认执行业务(如实际扣款、扣减库存)。
- Cancel 阶段:取消执行业务,释放预留资源(如解冻资金、回滚库存)。
实战代码(伪代码):
java
复制
// 支付服务 TCC 接口定义
public interface PaymentServiceTcc {
/**
* Try 阶段:检查账户余额,冻结资金
* @param userId 用户ID
* @param amount 金额
* @return true 成功, false 失败
*/
@TwoPhaseBusinessAction(name = "preparePayment", commitMethod = "confirmPayment", rollbackMethod = "cancelPayment")
boolean preparePayment(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount);
/**
* Confirm 阶段:真正扣款(从冻结余额扣除)
*/
boolean confirmPayment(BusinessActionContext context);
/**
* Cancel 阶段:释放冻结资金
*/
boolean cancelPayment(BusinessActionContext context);
}
考试论述要点:
- TCC 解决了跨服务调用的原子性问题。
- 缺点是需要编写三个阶段的代码,开发成本高,存在空回滚、悬挂等幂等性问题需要处理。
四、 核心方案二:基于 MQ 的可靠消息最终一致性(最常用)
支付系统中,订单服务和支付服务通常是异步解耦的。用户支付成功后,支付网关回调通知我们,我们需要更新订单状态。如果通知失败,需要利用 MQ 保证消息必达。
流程:
- 上游业务(支付成功)执行本地事务。
- 在本地事务内,将一条“待发送”消息写入本地消息表(与业务数据在同一库,利用本地事务保证原子性)。
- 定时任务轮询本地消息表,将消息发送到 MQ(如 RocketMQ)。
- 下游业务(订单服务)监听 MQ,消费消息,执行订单更新。
- 如果下游消费失败,利用 MQ 的重试机制进行重试,直至成功(死信队列人工介入)。
实战代码:
java
复制
// 1. 支付回调逻辑(本地事务 + 消息表)
@Transactional(rollbackFor = Exception.class)
public void handlePaymentSuccess(PaymentNotification notification) {
// A. 更新本地支付流水状态
paymentDAO.updateStatus(notification.getTradeNo(), Status.SUCCESS);
// B. 将需要通知订单服务的事件写入本地消息表
// 注意:这是和 A 在同一个本地事务中,要么都成功,要么都回滚
LocalMessage message = new LocalMessage();
message.setTopic("payment-success-topic");
message.setContent(JSON.toJSONString(notification));
message.setStatus("SENDING");
localMessageDAO.insert(message);
}
// 2. 独立的定时任务:投递消息
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<LocalMessage> messages = localMessageDAO.queryPendingMessages(100);
for (LocalMessage msg : messages) {
try {
// 发送到 RocketMQ
rocketMQTemplate.syncSend(msg.getTopic(), msg.getContent());
// 发送成功,更新状态为 SENT
localMessageDAO.updateStatus(msg.getId(), "SENT");
} catch (Exception e) {
// 发送失败,下次定时任务继续重试
log.error("Msg send failed: {}", msg.getId(), e);
}
}
}
// 3. 订单服务:消费消息
@RocketMQMessageListener(topic = "payment-success-topic", consumerGroup = "order-group")
public class OrderConsumer implements RocketMQListener<PaymentNotification> {
@Override
public void onMessage(PaymentNotification message) {
try {
// 更新订单状态
orderService.paid(message.getOrderId());
} catch (Exception e) {
// 抛出异常触发 MQ 重试
throw new RuntimeException("Consume failed, retry later...", e);
}
}
}
五、 总结:考试答题金句(可以直接背诵)
在论文或简答题结尾,使用以下总结升华:
“综上所述,在分布式支付系统的设计中,我们放弃了强一致性的 2PC 方案,转而采用了 BASE 理论(基本可用、软状态、最终一致性)。
针对内部服务,利用 Seata TCC 模式 实现资源的精确预留与释放;
针对跨系统的支付通知,采用 基于本地消息表的可靠消息最终一致性方案,利用 RocketMQ 的特性保证消息不丢失。这种‘柔性事务’的架构设计,在保证系统高可用性和高性能的同时,通过重试和幂等性校验机制,确保了数据的最终一致,符合大型互联网系统架构的最佳实践。”
避坑提示:
答题时千万别说“使用数据库的 XA 事务”,这在互联网高并发场景是负分项。一定要强调“最终一致性”和“异步解耦”。