一、概念:FSM(有限狀態(tài)機(jī)),模式之間轉(zhuǎn)換
狀態(tài)機(jī),也叫有限狀態(tài)機(jī)(FSM,F(xiàn)inite State Machine),是一種行為模式,是由一組定義良好的狀態(tài)、狀態(tài)之間的轉(zhuǎn)換規(guī)則和一個初始狀態(tài)組成。
- 根據(jù)當(dāng)前的狀態(tài)和輸入的事件,從一個狀態(tài)轉(zhuǎn)移到另一個狀態(tài)。
二、支付核心邏輯
2.1 支付交易三重奏:收單、結(jié)算、拒付款
下圖中我們可以看到,一共4種狀態(tài),每個狀態(tài)之間的轉(zhuǎn)換都通過指定事件觸發(fā)。

2.2 狀態(tài)機(jī)設(shè)計原則
無論是設(shè)計支付類的系統(tǒng),還是電商類的系統(tǒng),在設(shè)計狀態(tài)機(jī)時,都建議遵循以下原則
- 明確性:狀態(tài)和轉(zhuǎn)換必須清晰定義,避免含糊不清的狀態(tài)。
- 完備性:為所有可能的事件-狀態(tài)組合定義轉(zhuǎn)換邏輯。
- 可預(yù)測性:系統(tǒng)應(yīng)根據(jù)當(dāng)前狀態(tài)和給定事件可預(yù)測地響應(yīng)。
- 最小化:狀態(tài)數(shù)應(yīng)保持最小,避免不必要的復(fù)雜性。
常見誤區(qū)
- 過度設(shè)計:引入不必要的狀態(tài)
- 不完備的處理:沒有考慮到狀態(tài)與事件所有可能的轉(zhuǎn)換關(guān)系,導(dǎo)致系統(tǒng)行為不確定
- 硬編碼邏輯:過多硬編碼轉(zhuǎn)換邏輯,導(dǎo)致系統(tǒng)不具備可擴(kuò)展性和靈活性
比如下面的設(shè)計:
一眼看過去,好像除了復(fù)雜一點(diǎn),整體還是合理的,比如初始化,受理成功就到ACCEPT,然后到PAYING,如果直接成功就到PAIED,退款成功就到REFUND。
image.png
不合理的地方:
- 流程復(fù)雜:第一眼看過去會發(fā)現(xiàn)不那么清晰,流程比較繁瑣,比較復(fù)雜,有很多狀態(tài)都可以簡化或者舍去。比如ACCEPT沒有存在的必要。
- 職責(zé)不明確:支付單只管支付,到PAIED就算支付成功,最終狀態(tài)不再改變。不應(yīng)該后面還有REFUND狀態(tài)。REFUND應(yīng)該由退款單來負(fù)責(zé)處理,否則如果客戶部分退款,我們就不好處理了。
改進(jìn)方案:
- 刪除不必要的狀態(tài)。如:ACCEPT
- 將一個大型狀態(tài)機(jī)抽取為多份小的狀態(tài)機(jī)。比如把一些退款REFUND、請款等單據(jù)單獨(dú)抽取出來。這個樣子,雖然狀態(tài)機(jī)數(shù)量多了,但是每個狀態(tài)機(jī)都更加清晰明了。
1、主單
image.png
2、普通支付單
image.png
3、預(yù)授權(quán)單
image.png
4、請款單
image.png
5、退款單
image.png
最佳實(shí)踐及代碼規(guī)范
代碼層面:
- 分離狀態(tài)和處理邏輯:使用狀態(tài)模式,將每個狀態(tài)的行為都封裝在各自的類中
- 使用事件驅(qū)動模型:通過事件來觸發(fā)狀態(tài)轉(zhuǎn)換,而不是直接調(diào)用狀態(tài)方法
- 確保可追蹤性:狀態(tài)轉(zhuǎn)換應(yīng)被記錄和追蹤,以便故障排查和審計
上面幾點(diǎn)也就要求我們不應(yīng)該使用if else或者switch case來寫,會讓代碼看起來復(fù)雜。我們應(yīng)該將每個狀態(tài)封裝為單獨(dú)的類。
三、 Java版本實(shí)現(xiàn)
- 1、定義狀態(tài)基類
/**
* @Author 黃義波
* @Date 2024/12/9 14:15
* @Description 狀態(tài)基類
*/
public interface BaseStatus {
}
- 2、定義事件基類
/**
* @Author 黃義波
* @Date 2024/12/9 14:15
* @Description 事件基類
*/
public interface BaseEvent {
}
- 3、定義狀態(tài)-事件對,指定的狀態(tài)只能接受指定的事件
/**
* @Author 黃義波
* @Date 2024/12/9 14:15
* @Description 狀態(tài)事件對,指定的狀態(tài)只能接受指定的事件
*/
public class StatusEventPair<S extends BaseStatus, E extends BaseEvent> {
/**
* 指定的狀態(tài)
*/
private final S status;
/**
* 可接受的事件
*/
private final E event;
public StatusEventPair(S status, E event) {
this.status = status;
this.event = event;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof StatusEventPair) {
StatusEventPair<S, E> other = (StatusEventPair<S, E>)obj;
return this.status.equals(other.status) && this.event.equals(other.event);
}
return false;
}
@Override
public int hashCode() {
// 這里使用的是google的guava包。com.google.common.base.Objects
return Objects.hashCode(status, event);
}
}
- 4、定義狀態(tài)機(jī)
/**
* @Author 黃義波
* @Date 2024/12/9 14:18
* @Description 狀態(tài)機(jī)
*/
public class StateMachine<S extends BaseStatus, E extends BaseEvent> {
private final Map<StatusEventPair<S, E>, S> statusEventMap = new HashMap<>();
/**
* 只接受指定的當(dāng)前狀態(tài)下,指定的事件觸發(fā),可以到達(dá)的指定目標(biāo)狀態(tài)
*/
public void accept(S sourceStatus, E event, S targetStatus) {
statusEventMap.put(new StatusEventPair<>(sourceStatus, event), targetStatus);
}
/**
* 通過源狀態(tài)和事件,獲取目標(biāo)狀態(tài)
*/
public S getTargetStatus(S sourceStatus, E event) {
return statusEventMap.get(new StatusEventPair<>(sourceStatus, event));
}
}
- 5、定義支付狀態(tài)機(jī)。注:支付、退款等不同的業(yè)務(wù)狀態(tài)機(jī)是獨(dú)立的。
/**
* @Author 黃義波
* @Date 2024/12/9 14:23
* @Description 支付狀態(tài)機(jī)
*/
@AllArgsConstructor
@Getter
public enum PaymentStatus implements BaseStatus {
INIT("INIT", "初始化"),
PAYING("PAYING", "支付中"),
PAID("PAID", "支付成功"),
FAILED("FAILED", "支付失敗"),
;
// 支付狀態(tài)機(jī)內(nèi)容
private static final StateMachine<PaymentStatus, PaymentEvent> STATE_MACHINE = new StateMachine<>();
static {
// 初始狀態(tài)
STATE_MACHINE.accept(null, PaymentEvent.PAY_CREATE, INIT);
// 支付中
STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);
// 支付成功
STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_SUCCESS, PAID);
// 支付失敗
STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_FAIL, FAILED);
}
// 狀態(tài)
private final String status;
// 描述
private final String description;
/**
* 通過源狀態(tài)和事件類型獲取目標(biāo)狀態(tài)
*/
public static PaymentStatus getTargetStatus(PaymentStatus sourceStatus, PaymentEvent event) {
return STATE_MACHINE.getTargetStatus(sourceStatus, event);
}
}
- 6、定義支付事件。注:支付、退款等不同業(yè)務(wù)的事件是不一樣的。
/**
* @Author 黃義波
* @Date 2024/12/9 14:24
* @Description 支付事件
*/
@Getter
@AllArgsConstructor
public enum PaymentEvent implements BaseEvent {
// 支付創(chuàng)建
PAY_CREATE("PAY_CREATE", "支付創(chuàng)建"),
// 支付中
PAY_PROCESS("PAY_PROCESS", "支付中"),
// 支付成功
PAY_SUCCESS("PAY_SUCCESS", "支付成功"),
// 支付失敗
PAY_FAIL("PAY_FAIL", "支付失敗");
/**
* 事件
*/
private String event;
/**
* 事件描述
*/
private String description;
}
- 7、在支付單模型中聲明狀態(tài)和根據(jù)事件推進(jìn)狀態(tài)的方法
/**
* @Author 黃義波
* @Date 2024/12/9 14:24
* @Description 支付單模型
*/
@Data
public class PaymentModel {
/**
* 其它所有字段省略
*/
// 上次狀態(tài)
private PaymentStatus lastStatus;
// 當(dāng)前狀態(tài)
private PaymentStatus currentStatus;
/**
* 根據(jù)事件推進(jìn)狀態(tài)
*/
public void transferStatusByEvent(PaymentEvent event) {
// 根據(jù)當(dāng)前狀態(tài)和事件,去獲取目標(biāo)狀態(tài)
PaymentStatus targetStatus = PaymentStatus.getTargetStatus(currentStatus, event);
// 如果目標(biāo)狀態(tài)不為空,說明是可以推進(jìn)的
if (targetStatus != null) {
lastStatus = currentStatus;
currentStatus = targetStatus;
} else {
// 目標(biāo)狀態(tài)為空,說明是非法推進(jìn),進(jìn)入異常處理,這里只是拋出去,由調(diào)用者去具體處理
throw new StateMachineException(currentStatus, event, "狀態(tài)轉(zhuǎn)換失敗");
}
}
}
- 8、定義StateMachineException,也可以直接使用RuntimeException
/**
* @Author 黃義波
* @Date 2024/12/9 14:26
* @Description 狀態(tài)機(jī)異常
*/
@Data
public class StateMachineException extends RuntimeException {
private static final long serialVersionUID = 6610083281801529147L;
private Integer code;
private String message;
private PaymentEvent event;
private PaymentStatus status;
public StateMachineException(Integer code,String message) {
super(message);
this.code = code;
}
public StateMachineException(String message) {
super(message);
this.code = ErrorCodeEnum.ERROR.getCode();
}
public StateMachineException(PaymentStatus status, PaymentEvent event, String message) {
this.message = status.getDescription() + event.getDescription() + message;
this.code = ErrorCodeEnum.ERROR.getCode();
}
}
在支付業(yè)務(wù)代碼中的使用:只需要paymentModel.transferStatusByEvent(PaymentEvent.valueOf(message.getEvent()))
/**
* 支付領(lǐng)域域服務(wù)
*/
public class PaymentDomainServiceImpl implements PaymentDomainService {
/**
* 支付結(jié)果通知
*/
public void notify(PaymentNotifyMessage message) {
PaymentModel paymentModel = loadPaymentModel(message.getPaymentId());
try {
// 狀態(tài)推進(jìn)
paymentModel.transferStatusByEvent(PaymentEvent.valueOf(message.getEvent()));
savePaymentModel(paymentModel);
// 其它業(yè)務(wù)處理
... ...
} catch (StateMachineException e) {
// 異常處理
... ...
} catch (Exception e) {
// 異常處理
... ...
}
}
}
上面的代碼只需要加完善異常處理,優(yōu)化一下注釋,就可以直接用起來。
上面寫法的好處:
- 1、定義了明確的狀態(tài)、事件。
- 2、狀態(tài)機(jī)的推進(jìn),只能通過“當(dāng)前狀態(tài)、事件、目標(biāo)狀態(tài)”來推進(jìn),不能通過if else 或case switch來直接寫。比如:STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);
- 3、避免終態(tài)變更。比如線上碰到if else寫狀態(tài)機(jī),渠道異步通知比同步返回還快,異步通知回來把訂單更新為“PAIED”,然后同步返回的代碼把單據(jù)重新推進(jìn)到PAYING。
四、Golang版本實(shí)現(xiàn)
項(xiàng)目結(jié)構(gòu):

- 1、定義基礎(chǔ)狀態(tài)機(jī):base_state_machine.go
package model
type BaseStatus interface {
}
type BaseEvent interface {
}
type StatusEventPair struct {
status BaseStatus
event BaseEvent
}
func (pair StatusEventPair) equals(other StatusEventPair) bool {
return pair.status == other.status && pair.event == other.event
}
type StateMachine struct {
statusEventMap map[StatusEventPair]BaseStatus
}
func (sm *StateMachine) accept(sourceStatus BaseStatus, event BaseEvent, targetStatus BaseStatus) {
pair := StatusEventPair{status: sourceStatus, event: event}
sm.statusEventMap[pair] = targetStatus
}
func (sm *StateMachine) getTargetStatus(sourceStatus BaseStatus, event BaseEvent) BaseStatus {
pair := StatusEventPair{status: sourceStatus, event: event}
baseStatus := sm.statusEventMap[pair]
return baseStatus
}
- 2、定義支付狀態(tài)機(jī):payment_state_machine.go
package model
type PaymentStatus string
const (
INIT PaymentStatus = "INIT"
PAYING PaymentStatus = "PAYING"
PAID PaymentStatus = "PAID"
FAILED PaymentStatus = "FAILED"
)
type PaymentEvent string
const (
PAY_CREATE PaymentEvent = "PAY_CREATE"
PAY_PROCESS PaymentEvent = "PAY_PROCESS"
PAY_SUCCESS PaymentEvent = "PAY_SUCCESS"
PAY_FAIL PaymentEvent = "PAY_FAIL"
)
var PaymentStateMachine = StateMachine{statusEventMap: map[StatusEventPair]BaseStatus{}}
func init() {
//支付狀態(tài)機(jī)初始化,包含所有可能的情況
PaymentStateMachine.accept(nil, PAY_CREATE, INIT)
PaymentStateMachine.accept(INIT, PAY_PROCESS, PAYING)
PaymentStateMachine.accept(PAYING, PAY_SUCCESS, PAID)
PaymentStateMachine.accept(PAYING, PAY_FAIL, FAILED)
}
func GetTargetStatus(sourceStatus PaymentStatus, event PaymentEvent) PaymentStatus {
status := PaymentStateMachine.getTargetStatus(sourceStatus, event)
if status != nil {
return status.(PaymentStatus)
}
panic("獲取目標(biāo)狀態(tài)失敗")
}
type PaymentModel struct {
lastStatus PaymentStatus
CurrentStatus PaymentStatus
}
func (pm *PaymentModel) TransferStatusByEvent(event PaymentEvent) {
targetStatus := GetTargetStatus(pm.CurrentStatus, event)
if targetStatus != "" {
pm.lastStatus = pm.CurrentStatus
pm.CurrentStatus = targetStatus
} else {
// 處理異常
panic("狀態(tài)轉(zhuǎn)換失敗")
}
}
- 3、使用及測試:main.go:
package main
import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"github.com/ziyifast/log"
"myTest/demo_home/state_machine_demo/model"
"time"
)
var (
testOrder = new(model.PaymentModel)
)
func main() {
application := iris.New()
application.Get("/order/create", createOrder)
application.Get("/order/pay", payOrder)
application.Get("/order/status", getOrderStatus)
application.Listen(":8899", nil)
}
func createOrder(context *context.Context) {
testOrder.CurrentStatus = model.INIT
context.WriteString("create order succ...")
}
func payOrder(context *context.Context) {
testOrder.TransferStatusByEvent(model.PAY_PROCESS)
log.Infof("call third api....")
//調(diào)用第三方支付接口和其他業(yè)務(wù)處理邏輯
time.Sleep(time.Second * 15)
log.Infof("done...")
testOrder.TransferStatusByEvent(model.PAY_SUCCESS)
}
func getOrderStatus(context *context.Context) {
context.WriteString(string(testOrder.CurrentStatus))
}
聲明:為了快速驗(yàn)證以及讓代碼更加簡潔,沒有按照標(biāo)準(zhǔn)的規(guī)范來編寫controller、service、dao等。
五、測試:
- 1、啟動程序,調(diào)用create接口,創(chuàng)建訂單
http://localhost:8899/order/create
image.png
- 2、調(diào)用支付接口支付訂單
我們手動模擬調(diào)用第三方支付接口,sleep了幾十秒(實(shí)際調(diào)用肯定比這個快多了),所以不會立即返回結(jié)果,我們需要新開一個窗口,直接查詢訂單狀態(tài)
image.png
- 3、立即調(diào)用查詢接口獲取訂單狀態(tài),查看是否為支付中
http://localhost:8899/order/status
image.png
- 4、等待支付成功后,調(diào)用接口查看訂單狀態(tài),是否為已支付
等待后臺日志打印done之后重新調(diào)用查詢接口:
image.png
http://localhost:8899/order/status
image.png
六、并發(fā)更新問題:多線程修改同一狀態(tài)機(jī)(db版本號)
狀態(tài)機(jī)領(lǐng)域模型同時被兩個線程操作怎么避免狀態(tài)冪等問題
這是一個好問題。在分布式場景下,這種情況太過于常見。同一機(jī)器有可能多個線程處理同一筆業(yè)務(wù),不同機(jī)器也可能處理同一筆業(yè)務(wù)。
業(yè)內(nèi)通常的做法是設(shè)計良好的狀態(tài)機(jī) + 數(shù)據(jù)庫鎖 + 數(shù)據(jù)版本號解決。
image.png
簡要說明:
- 狀態(tài)機(jī)一定要設(shè)計好,只有特定的原始狀態(tài) + 特定的事件才可以推進(jìn)到指定的狀態(tài)。比如 INIT + 支付成功才能推進(jìn)到sucess。
- 更新數(shù)據(jù)庫之前,先使用select for update進(jìn)行鎖行記錄,同時在更新時判斷版本號是否是之前取出來的版本號,更新成功就結(jié)束,更新失敗就組成消息發(fā)到消息隊列,后面再消費(fèi)。
- 通過補(bǔ)償機(jī)制兜底,比如查詢補(bǔ)單。
通過上述三個步驟,正常情況下,最終的數(shù)據(jù)狀態(tài)一定是正確的。除非是某個系統(tǒng)有異常,比如外部渠道開始返回支付成功,然后又返回支付失敗,說明依賴的外部系統(tǒng)已經(jīng)異常,這樣只能進(jìn)人工差錯處理流程。
?
參考:
https://blog.csdn.net/weixin_45565886/article/details/137651521











