一、前言:那些年我们在支付系统里踩过的坑
说实话,写这篇内容的时候,我还在回想三年前那个深夜。那时候我们的支付系统突然报警,说是有一笔订单状态不对,用户付了钱但订单还是“待支付”。更可怕的是,这种问题不是偶发的,每隔几天就会冒出来一次,每次都要花几个小时去查日志、对账、甚至联系财务退款。
那个阶段,我们团队每天活在恐惧中。老板问:“今天有没有资损?”财务问:“这笔钱去哪了?”技术团队则在排查各种诡异的问题:有的用户支付成功但订单未更新,有的用户被扣款两次,有的退款金额不对……
今天,我想把这些年的血泪经验整理出来,分享给大家。这不是什么高大上的架构设计论文,而是一个从0到1搭建支付中台过程中,关于分布式事务、丢单问题、幂等性校验和资损防控的真实实战分享。
二、支付系统的核心挑战:为什么这么难?
2.1 支付系统的特殊性
支付系统和其他业务系统最大的不同在于:它直接涉及金钱。
在其他系统中,数据错了可以修正,状态不对可以重置。但在支付系统中,任何错误都可能导致:
- 用户多扣钱(资损)
- 用户少付款(资损)
- 订单状态不一致(业务混乱)
- 重复支付(用户体验极差)
这些问题不仅影响用户体验,更会影响公司的财务健康和法律合规。
2.2 分布式环境下的三大难题
在单体架构中,我们可以用数据库事务来保证一致性。但在分布式系统中,我们面临三个核心难题:
1. 分布式事务的复杂性 当支付请求需要调用多个服务(订单服务、账户服务、支付网关、风控服务等)时,如何保证这些操作要么全部成功,要么全部失败?
2. 网络不稳定性导致的丢单 支付请求可能因为网络超时、服务重启、中间件故障等原因丢失,如何确保每一个支付请求都能被正确处理?
3. 幂等性的保证 用户可能因为网络卡顿多次点击支付按钮,支付网关可能因为重试机制重复调用我们的接口,如何确保同一笔支付只处理一次?
三、架构设计:支付中台的核心组件
3.1 整体架构图
在设计支付中台时,我们采用了分层架构,核心组件包括:
┌─────────────────────────────────────────────────────────────┐
│ 接入层 (API Gateway) │
├─────────────────────────────────────────────────────────────┤
│ 支付服务层 (Payment Service) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 支付受理 │ │ 支付路由 │ │ 支付结果 │ │
│ │ Service │ │ Service │ │ Service │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 核心处理层 (Core Processing) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 分布式事务 │ │ 幂等性控制 │ │ 状态机引擎 │ │
│ │ Manager │ │ Manager │ │ (State │ │
│ │ │ │ │ │ Engine) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 数据存储层 (Data Layer) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 支付订单 │ │ 支付记录 │ │ 对账数据 │ │
│ │ (MySQL) │ │ (MySQL) │ │ (MySQL) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 基础设施层 (Infrastructure) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Redis缓存 │ │ Kafka消息 │ │ 分布式锁 │ │
│ │ │ │ 队列 │ │ (Redis) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
3.2 核心表结构设计
数据库设计是支付系统的基石。我们的核心表包括:
支付订单表 (payment_order)
CREATE TABLE `payment_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_no` varchar(64) NOT NULL COMMENT '业务订单号',
`payment_no` varchar(64) NOT NULL COMMENT '支付流水号',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '支付金额(分)',
`currency` varchar(8) NOT NULL DEFAULT 'CNY' COMMENT '币种',
`payment_channel` varchar(32) NOT NULL COMMENT '支付渠道',
`payment_type` varchar(32) NOT NULL COMMENT '支付类型',
`status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '订单状态: 0-待支付 1-支付中 2-支付成功 3-支付失败 4-已关闭',
`prepay_id` varchar(128) DEFAULT NULL COMMENT '预支付ID',
`transaction_id` varchar(64) DEFAULT NULL COMMENT '渠道交易号',
`expire_time` datetime NOT NULL COMMENT '过期时间',
`success_time` datetime DEFAULT NULL COMMENT '支付成功时间',
`close_time` datetime DEFAULT NULL COMMENT '订单关闭时间',
`error_code` varchar(32) DEFAULT NULL COMMENT '错误码',
`error_msg` varchar(256) DEFAULT NULL COMMENT '错误信息',
`version` int(11) NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` tinyint(4) NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删除 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_payment_no` (`payment_no`),
KEY `idx_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status_create_time` (`status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';
支付流水表 (payment_transaction)
CREATE TABLE `payment_transaction` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`transaction_no` varchar(64) NOT NULL COMMENT '交易流水号',
`payment_no` varchar(64) NOT NULL COMMENT '支付流水号',
`transaction_type` varchar(32) NOT NULL COMMENT '交易类型: PAY-支付 REFUND-退款 CLOSE-关闭',
`amount` bigint(20) NOT NULL COMMENT '交易金额(分)',
`channel_fee` bigint(20) NOT NULL DEFAULT 0 COMMENT '渠道手续费(分)',
`actual_amount` bigint(20) NOT NULL COMMENT '实际支付金额(分)',
`status` tinyint(4) NOT NULL COMMENT '交易状态: 0-处理中 1-成功 2-失败',
`channel_response` text COMMENT '渠道响应信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_transaction_no` (`transaction_no`),
KEY `idx_payment_no` (`payment_no`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付交易流水表';
对账文件表 (reconciliation_file)
CREATE TABLE `reconciliation_file` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`file_name` varchar(256) NOT NULL COMMENT '文件名',
`file_path` varchar(512) NOT NULL COMMENT '文件路径',
`file_type` varchar(32) NOT NULL COMMENT '文件类型: DAILY-日结算 CLEARING-清算',
`channel` varchar(32) NOT NULL COMMENT '支付渠道',
`reconciled_date` date NOT NULL COMMENT '对账日期',
`total_amount` bigint(20) NOT NULL DEFAULT 0 COMMENT '文件总金额',
`total_count` int(11) NOT NULL DEFAULT 0 COMMENT '文件总笔数',
`status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态: 0-待对账 1-对账中 2-对账完成 3-对账异常',
`diff_count` int(11) NOT NULL DEFAULT 0 COMMENT '差异笔数',
`error_msg` text COMMENT '错误信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_file_name_date` (`file_name`, `reconciled_date`),
KEY `idx_channel_date` (`channel`, `reconciled_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对账文件表';
四、分布式事务解决方案:我们如何保证数据一致性
4.1 分布式事务的演进历程
在我们最初的架构中,我们尝试过多种方案:
第一阶段:本地事务 + 补偿机制
@Service
public class PaymentServiceV1 {
@Autowired
private PaymentOrderMapper orderMapper;
@Autowired
private UserAccountMapper accountMapper;
public PaymentResult createPayment(String orderNo, Long userId, Integer amount) {
// 1. 创建支付订单
PaymentOrder order = new PaymentOrder();
order.setOrderNo(orderNo);
order.setUserId(userId);
order.setAmount(amount);
order.setStatus(PaymentOrderStatus.PENDING);
orderMapper.insert(order);
try {
// 2. 调用支付渠道
String prepayId = paymentGateway.createPrepay(orderNo, amount);
order.setPrepayId(prepayId);
order.setStatus(PaymentOrderStatus.PAYING);
orderMapper.updateById(order);
// 3. 锁定用户账户
accountMapper.lockAccount(userId, amount);
return PaymentResult.success(order.getPaymentNo(), prepayId);
} catch (Exception e) {
// 4. 补偿:回滚账户锁定,删除订单
accountMapper.unlockAccount(userId, amount);
orderMapper.deleteById(order.getId());
throw new PaymentException("支付创建失败", e);
}
}
}
问题:如果步骤3成功但步骤4失败,就会出现账户被锁定但订单被删除的情况。而且,补偿操作本身也可能失败。
第二阶段:引入消息队列 + 最终一致性
@Service
public class PaymentServiceV2 {
@Autowired
private PaymentOrderMapper orderMapper;
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
public PaymentResult createPayment(String orderNo, Long userId, Integer amount) {
// 1. 创建支付订单
PaymentOrder order = new PaymentOrder();
order.setOrderNo(orderNo);
order.setUserId(userId);
order.setAmount(amount);
order.setStatus(PaymentOrderStatus.PENDING);
orderMapper.insert(order);
try {
// 2. 调用支付渠道
String prepayId = paymentGateway.createPrepay(orderNo, amount);
order.setPrepayId(prepayId);
order.setStatus(PaymentOrderStatus.PAYING);
orderMapper.updateById(order);
// 3. 发送支付初始化消息(本地事务 + 消息发送)
PaymentInitMessage message = new PaymentInitMessage();
message.setPaymentNo(order.getPaymentNo());
message.setUserId(userId);
message.setAmount(amount);
message.setOrderNo(orderNo);
kafkaTemplate.send("payment-init-topic", message);
return PaymentResult.success(order.getPaymentNo(), prepayId);
} catch (Exception e) {
orderMapper.deleteById(order.getId());
throw new PaymentException("支付创建失败", e);
}
}
}
问题:虽然解决了账户锁定和订单创建的一致性,但如果消息发送失败,订单状态就是”PENDING”,而实际上支付渠道已经创建了预支付订单。而且,如果消息消费失败,如何重试?
第三阶段:我们最终的方案——Seata分布式事务 + TCC模式
经过多次迭代,我们最终选择了Seata框架,采用TCC(Try-Confirm-Cancel)模式来处理分布式事务。
4.2 TCC模式的实现细节
TCC模式将分布式事务分为三个阶段:
- Try阶段:完成业务检查及一致性资源预留
- Confirm阶段:确认执行业务操作,使用Try阶段预留的资源
- Cancel阶段:释放Try阶段预留的资源
”`java /**
TCC支付服务接口定义 */ public interface PaymentTccService {
/**
- Try阶段:尝试冻结账户资金,创建支付订单 */ void tryPayment(String paymentNo, String orderNo, Long userId, Integer amount);
/**
- Confirm阶段:确认支付,扣减账户余额 */ void confirmPayment(String paymentNo);
/**
- Cancel阶段:取消支付,解冻账户资金 */ void cancelPayment(String paymentNo); }
/**
TCC支付服务实现 */ @Service public class PaymentTccServiceImpl implements PaymentTccService {
@Autowired private PaymentOrderMapper orderMapper;
@Autowired private UserAccountMapper accountMapper;
@Autowired private PaymentGateway paymentGateway;
@Autowired private SeataTransactionManager transactionManager;
/**
Try阶段实现 */ @Override @GlobalTransactional public void tryPayment(String paymentNo, String orderNo, Long userId, Integer amount) { // 1. 创建支付订单 PaymentOrder order = buildPaymentOrder(paymentNo, orderNo, userId, amount); orderMapper.insert(order);
// 2. 调用支付渠道创建预支付订单 String prepayId = paymentGateway.createPrepay(orderNo, amount); order.setPrepayId(prepayId); order.setStatus(PaymentOrderStatus.PAYING); orderMapper.updateById(order);
// 3. 冻结用户账户资金(使用分布式锁) String lockKey = “payment:freeze:” + userId; RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 再次检查账户余额 AccountBalance balance = accountMapper.getBalance(userId); if (balance.getAvailableAmount() < amount) { throw new PaymentException("账户余额不足"); } // 冻结资金 accountMapper.freezeAmount(userId, amount); // 更新订单状态 order.setStatus(PaymentOrderStatus.FROZEN); orderMapper.updateById(order); } else { throw new PaymentException("获取分布式锁失败"); }} catch (InterruptedException e) {
Thread.currentThread().interrupt(); throw new PaymentException("线程被中断", e);} finally {
if (lock.isHeldByCurrentThread()) { lock.unlock(); }} }
/**
Confirm阶段实现 */ @Override public void confirmPayment(String paymentNo) { // 1. 查询支付订单 PaymentOrder order = orderMapper.getByPaymentNo(paymentNo); if (order == null) {
throw new PaymentException("支付订单不存在");}
// 2. 幂等性检查 if (order.getStatus() == PaymentOrderStatus.SUCCESS
|| order.getStatus() == PaymentOrderStatus.CONFIRMED) { log.info("支付订单{}已处理,跳过confirm", paymentNo); return;}
// 3. 扣减账户余额 accountMapper.deductAmount(order.getUserId(), order.getAmount());
// 4. 更新订单状态 order.setStatus(PaymentOrderStatus.SUCCESS); order.setSuccessTime(new Date()); orderMapper.updateById(order);
// 5. 记录支付流水 recordTransaction(order, TransactionType.PAY);
// 6. 发送支付成功消息 sendMessage(“payment-success-topic”, buildSuccessMessage(order));
log.info(“支付订单{}确认成功”, paymentNo); }
/**
- Cancel阶段实现 */ @Override public void cancelPayment(String paymentNo) {
