状态机在支付中的应用
一、什么是状态机(有限状态机 FSM)
简单一句话:一个支付单,在任意时刻只有唯一状态,只能被指定事件驱动,流向预先定义好的下一个状态。
有限状态机 = 状态 + 事件(用户支付) + 动作(支付中改完成) + 流转规则
二、支付业务为什么必须用状态机?
支付的痛点:
- 异步回调(微信异步通知,晚几秒回来)
- 重复请求、重复回调
- 并发请求(同时退款+支付回调)
如果用简单 update set status=xx 裸更新容易出现:脏数据、状态被覆盖
状态机的价值:
- 强约束流转,非法状态变更直接拒绝
- 幂等防重:同一个事件多次进来,只处理一次
- 把复杂的分支if-else收拢成统一流转规则
三、支付订单标准状态流转(最经典的一套)
-
支付单基础状态枚举
-
INIT 初始化/待支付
-
PAYING 支付处理中(已调起三方收银台)
-
PAY_SUCCESS 支付成功(收到三方成功回调)
-
PAY_FAIL 支付失败(余额不足、银行卡拒绝)
-
REFUNDING 退款中
-
REFUNDED 已全额退款
-
CLOSED 超时关闭 / 主动关闭订单
-
合法流转(简化版)
-
INIT
- 触发【发起支付】→ PAYING
- 触发【超时关闭】→ CLOSED
- PAYING
- 三方回调【支付成功】→ PAY_SUCCESS
- 三方回调【支付失败】→ PAY_FAIL
- 超时未支付 → CLOSED
- PAY_SUCCESS(终态之一,但支持退款分支)
- 触发【申请退款】→ REFUNDING
- REFUNDING
- 退款回调成功 → REFUNDED
- 退款失败 → 退回 PAY_SUCCESS(可重试退款)
重点:
不允许:已关闭的订单再被改成支付成功;不允许支付失败单直接退款
四、工程落地的三种实现方式(后端开发常用)
方式1:数据库状态机(最常用,分布式支付系统首选)
数据表字段:
pay_order(id, order_no, status, version, ...)
更新SQL必须带前置状态条件(乐观锁+状态前置判断)
伪代码:
// 正确:只能从 PAYING 状态,被支付成功事件改成 PAY_SUCCESS
int rows = update pay_order set status = PAY_SUCCESS, pay_time = now()
where order_no = ? and status = PAYING and version = ?;
if(rows == 0) {
// 状态已经变了(被别的请求处理过了),拒绝执行,防止重复回调覆盖
return 幂等跳过
}
这就是数据库层面的有限状态机。
方式2:框架状态机(Spring StateMachine / 自研状态机引擎)
把配置写在配置类/配置文件里:
state(INIT)
.on(Event.PAY_SUBMIT).target(PAYING)
state(PAYING)
.on(Event.PAY_CALLBACK_SUCCESS).target(PAY_SUCCESS)
优点:流转规则集中配置,可读性强,新增状态只改配置
缺点:分布式场景下,还是要依赖DB做持久化。
方式3:状态机 + 本地消息表 / 定时补偿任务
支付是异步的:
- 收到回调 → 驱动状态机流转
- 定时任务扫描:卡在 PAYING 很久的单子,主动查询三方接口对账,驱动状态机修正状态
解决回调丢失问题。
五、支付里几个典型的状态机应用场景
场景1:支付回调幂等防重
三方支付网关可能重复推送回调:
- 第一次回调:PAYING → PAY_SUCCESS(更新成功,执行业务发券、记账)
- 第二次重复回调:当前状态已经是 PAY_SUCCESS,状态机判定:不允许再次流转,直接返回success,不重复执行业务。
场景2:部分退款 / 多次退款状态控制
订单支付成功后:
- 第一次部分退款:状态进入退款中,记录已退金额
- 第二次退款:状态机校验:剩余可退金额>0,才允许发起二次退款
防止超退、超额退款资损。
场景3:关单超时自动关闭
未支付订单30分钟超时:
定时任务触发【超时事件】
状态机判断:当前状态是INIT/PAYING → 流转到CLOSED
如果已经是支付成功,拒绝关单。
场景4:对账与差错处理
日终对账:
本地库状态 vs 微信/支付宝对账文件不一致
人工/补偿任务触发【差错修复事件】,在管理员权限的状态机规则下修正状态。
六、设计要点
- 状态设计要粗粒度,不要太细碎
不要拆出几十个细碎中间态,增加维护成本。 - 区分:终态 / 中间态
终态:已支付、已全额退款、已关闭(一般不再主动流转)
中间态:支付中、退款中(等待外部回调) - 状态机 + 乐观锁/分布式锁 一起使用
防止并发下两个请求同时驱动状态流转。 - 状态变更必须留流水日志(状态变更日志表)
order_state_log(订单号,旧状态,新状态,事件,操作时间,来源(回调/定时/人工))
支付审计、排查资损必备。
七、精简总结
- 状态机由状态、事件、动作组成,约束对象只能按照预设规则流转状态。
- 支付系统中,解决异步回调、重复请求、并发更新导致的状态混乱与资损风险。
- 落地一般是:数据库状态前置判断 + 乐观锁 + 状态变更日志 + 定时补偿。
- 支付核心流转:初始化→支付中→支付成功(失败/超时)→退款中→已退款→关闭
- 核心价值:状态强约束、幂等、可追溯、减少大量if-else分支。
浙公网安备 33010602011771号