2. 支付订单状态机设计:订单状态流转与幂等作用

支付订单状态机是防重支付方案的核心骨架。一个设计良好的状态机,能够从业务语义和系统约束两个层面阻止重复支付:业务语义上,状态机明确“一笔订单只能从待支付流转到支付中,再最终到达已支付”;系统约束上,状态机在数据库层面提供行级锁或乐观锁,确保并发环境下只有一次状态变更成功。

2.1 支付订单核心状态定义

在电商、金融、充值等场景中,支付订单通常包含以下四个核心状态:

状态 状态码 语义说明 允许的后续状态
待支付 PENDING 订单已创建,等待用户发起支付 支付中
支付中 PROCESSING 支付请求已提交,正在等待渠道回调 已支付、待支付(超时回滚)
已支付 PAID 支付成功,资金已确认到账 已退款(终态,不可再支付)
已退款 REFUNDED 订单已全额或部分退款 无(终态)
设计要点:“支付中”状态是防重支付的关键缓冲层。它明确告知系统“该订单的支付请求正在处理中”,后续任何重复的支付请求都应被拒绝或排队等待,而不是直接进入“已支付”。

2.2 状态流转图:正向与逆向路径

下图展示了支付订单的完整状态机流转路径,包含正向支付流程和逆向退款流程:

支付订单状态机流转图 支付订单状态机流转图 待支付 PENDING 支付中 PROCESSING 已支付 PAID 已退款 REFUNDED 发起支付 支付成功回调 超时/取消回滚 发起退款 说明: 只有“待支付”状态允许发起支付;“支付中”是防重缓冲状态,重复支付请求在此被拦截; “已支付”和“已退款”为终态,不可逆向回退。

2.3 状态机在幂等中的核心作用

状态机不是简单的状态枚举,它是幂等防重方案的最后一道防线。其作用体现在以下三个层面:

  1. 状态前置校验:在执行业务逻辑前,先检查订单当前状态。只有 PENDING 状态的订单才允许进入支付流程;如果订单已经是 PAID 或 PROCESSING,直接返回“重复请求”错误。
  2. 乐观锁状态更新:使用带条件更新的 SQL(UPDATE ... WHERE status = 'PENDING'),确保并发环境下只有一个请求能成功将状态从 PENDING 更新为 PROCESSING。更新影响行数为 0 时,说明状态已被其他请求变更,当前请求应视为重复。
  3. 状态机驱动幂等键:幂等键(Idempotent Key)的生成和验证可以结合状态机。例如,幂等键 = orderId + currentStatus,确保同一订单在不同状态下使用不同的幂等键,避免状态回滚后幂等键被错误复用。
核心原则:状态机与分布式锁是互补关系。分布式锁解决“同一时刻只有一个请求能操作订单”的并发问题;状态机解决“即使锁被绕过,状态约束也能阻止非法流转”的语义问题。两者结合,形成双重防护。

2.4 状态变更的原子性操作

在支付系统中,状态变更必须与业务操作绑定在同一个数据库事务中,保证原子性。以下是一个典型的支付成功回调处理流程:

@Transactional
public void handlePaymentSuccess(String orderId, String channelTradeNo) {
    // 1. 查询订单(带行级锁)
    Order order = orderMapper.selectForUpdate(orderId);
    
    // 2. 状态机前置校验
    if (!OrderStatus.PROCESSING.equals(order.getStatus())) {
        throw new RepeatPaymentException("订单状态不允许支付成功回调");
    }
    
    // 3. 更新订单状态和业务字段
    order.setStatus(OrderStatus.PAID);
    order.setChannelTradeNo(channelTradeNo);
    order.setPaidTime(new Date());
    orderMapper.updateById(order);
    
    // 4. 发送支付成功消息(事务消息或本地消息表)
    eventPublisher.publish(new PaymentSuccessEvent(orderId));
}
避坑提示:不要使用“先查询再判断再更新”的非原子操作。即使查询时状态为 PROCESSING,在并发环境下,另一个线程可能已经将状态更新为 PAID。必须使用 SELECT ... FOR UPDATE 或乐观锁 UPDATE ... WHERE status = 'PROCESSING' 来保证原子性。

2.5 状态机与分布式锁的协作关系

下表总结了状态机与分布式锁在防重支付方案中的职责分工:

维度 分布式锁 状态机
作用层级 应用层并发控制 数据层语义约束
防护目标 防止同一时刻多个请求同时操作 防止非法状态流转(如已支付再次支付)
失效场景 锁超时、锁误删、Redis 宕机 数据库事务隔离级别不足、乐观锁冲突
互补关系 锁失效时,状态机仍可拦截重复 状态机失效时,锁可减少并发冲突概率
推荐组合 先获取分布式锁,再执行状态机校验 状态机校验通过后,再执行数据库更新

下图展示了分布式锁与状态机在支付请求处理中的协作流程:

分布式锁与状态机协作流程图 分布式锁与状态机协作流程图 支付请求到达 获取分布式锁 状态机前置校验 执行支付逻辑 协作要点: 1. 锁失败 → 直接返回重复请求 2. 锁成功但状态校验失败 → 释放锁,返回重复 3. 两者都通过 → 执行支付,释放锁

2.6 状态机设计的最佳实践

  • 状态枚举集中管理:将订单状态定义在统一的枚举类中,避免业务代码中散落字符串常量。枚举类同时维护状态流转规则(如 canTransition(from, to) 方法)。
  • 状态变更记录审计:每次状态变更都写入状态变更日志表,记录 order_id、from_status、to_status、operator、operate_time。这为问题排查和幂等追溯提供完整证据链。
  • 终态不可逆:PAID 和 REFUNDED 作为终态,任何业务逻辑都不允许从终态回退到非终态。退款操作只能从 PAID 流转到 REFUNDED,不能从 REFUNDED 再回到 PAID。
  • 超时回滚机制:对于长时间停留在 PROCESSING 状态的订单,需要定时任务扫描并回滚到 PENDING 状态,避免订单“卡死”。回滚时需校验支付渠道是否已回调,防止资金损失。
总结:支付订单状态机是防重支付方案的“语义防火墙”。它从业务规则层面杜绝了重复支付的可能性,与分布式锁的“并发防火墙”形成互补。在设计状态机时,务必关注原子性、终态不可逆和审计日志三个关键点。