Java通用型支付+电商平台双系统实战 | 完结

4 阅读4分钟

实战支付系统:分布式事务在考试中的标准回答模板

一、 问题背景(考试答题第一步)

场景描述:
在典型的支付业务中,我们需要同时操作两个独立的系统:本地交易数据库(记录订单状态)和外部支付网关(如支付宝、微信支付)或库存服务
核心矛盾:
由于微服务架构下数据库被拆分,传统的 ACID 事务(本地数据库事务)无法跨库生效。如果本地订单扣款成功,但调用外部支付网关失败,或者库存服务回滚失败,就会导致数据不一致(如:钱扣了,订单却未成功)。为了解决此问题,必须引入分布式事务解决方案。

二、 解决方案核心策略(答题得分的“骨架”)

在支付场景中,由于涉及外部系统(银行/第三方),我们通常无法锁定其资源,因此 2PC(两阶段提交)  和 3PC(三阶段提交)  因其阻塞性强、性能差,很少直接用于支付链路。

标准答案是:

  1. 对于内部服务间的一致性(如订单-库存):  推荐使用 TCC (Try-Confirm-Cancel)  或 Seata 的 AT 模式
  2. 对于涉及外部支付的一致性(如订单-支付网关):  推荐使用 基于消息队列的最终一致性方案(可靠消息最终一致性)

三、 核心方案一: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 保证消息必达。

流程:

  1. 上游业务(支付成功)执行本地事务。
  2. 在本地事务内,将一条“待发送”消息写入本地消息表(与业务数据在同一库,利用本地事务保证原子性)。
  3. 定时任务轮询本地消息表,将消息发送到 MQ(如 RocketMQ)。
  4. 下游业务(订单服务)监听 MQ,消费消息,执行订单更新。
  5. 如果下游消费失败,利用 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 事务”,这在互联网高并发场景是负分项。一定要强调“最终一致性”和“异步解耦”。