支付系統(tǒng)核心邏輯 — — 狀態(tài)機(jī)(Java&Golang版本)

一、概念: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ā)。


image.png

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):


image.png
  • 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)用支付接口支付訂單

http://localhost:8899/order/pay

我們手動模擬調(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容