支付系统核心逻辑 — — 状态机

代码地址:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/state_machine_demo

1 概念:FSM(有限状态机),模式之间转换

状态机,也叫有限状态机(FSM,Finite State Machine),是一种行为模式,是由一组定义良好的状态、状态之间的转换规则和一个初始状态组成。

根据当前的状态和输入的事件,从一个状态转移到另一个状态。

2 实战:支付核心逻辑

2.1 支付交易三重奏:收单、结算、拒付款

下图中我们可以看到,一共4种状态,每个状态之间的转换都通过指定事件触发。

2.2 状态机设计原则

无论是设计支付类的系统,还是电商类的系统,在设计状态机时,都建议遵循以下原则

明确性:状态和转换必须清晰定义,避免含糊不清的状态。完备性:为所有可能的事件-状态组合定义转换逻辑。可预测性:系统应根据当前状态和给定事件可预测地响应。最小化:状态数应保持最小,避免不必要的复杂性。

①明确性:状态与转换必须定义清晰

②完备性:需要考虑所有事件-状态的转换组合

③可预测性:需根据当前状态+给定事件可预测响应

④最小化:状态数要少,避免过于复杂

常见误区

过度设计:引入不必要的状态不完备的处理:没有考虑到状态与事件所有可能的转换关系,导致系统行为不确定硬编码逻辑:过多硬编码转换逻辑,导致系统不具备可扩展性和灵活性

比如下面的设计:

一眼看过去,好像除了复杂一点,整体还是合理的,比如初始化,受理成功就到ACCEPT,然后到PAYING,如果直接成功就到PAIED,退款成功就到REFUND。

不合理的地方:

流程复杂。第一眼看过去会发现不那么清晰,流程比较繁琐,比较复杂,有很多状态都可以简化或者舍去。比如ACCEPT没有存在的必要。职责不明确。支付单只管支付,到PAIED就算支付成功,最终状态不再改变。不应该后面还有REFUND状态。REFUND应该由退款单来负责处理,否则如果客户部分退款,我们就不好处理了。

改进方案:

删除不必要的状态。如:ACCEPT将一个大型状态机抽取为多份小的状态机。比如把一些退款REFUND、请款等单据单独抽取出来。这个样子,虽然状态机数量多了,但是每个状态机都更加清晰明了。

主单:

普通支付单

预授权单

请款单

退款单

最佳实践及代码规范

代码层面:

分离状态和处理逻辑:使用状态模式,将每个状态的行为都封装在各自的类中使用事件驱动模型:通过事件来触发状态转换,而不是直接调用状态方法确保可追踪性:状态转换应被记录和追踪,以便故障排查和审计

上面几点也就要求我们不应该使用if else或者switch case来写,会让代码看起来复杂。我们应该将每个状态封装为单独的类。

2.3 Java版本实现

定义状态基类

/**

* 状态基类

*/

public interface BaseStatus {

}

定义事件基类

/**

* 事件基类

*/

public interface BaseEvent {

}

定义状态-事件对,指定的状态只能接受指定的事件

/**

* 状态事件对,指定的状态只能接受指定的事件

*/

public class StatusEventPair {

/**

* 指定的状态

*/

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 other = (StatusEventPair)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);

}

}

定义状态机

/**

* 状态机

*/

public class StateMachine {

private final Map, S> statusEventMap = new HashMap<>();

/**

* 只接受指定的当前状态下,指定的事件触发,可以到达的指定目标状态

*/

public void accept(S sourceStatus, E event, S targetStatus) {

statusEventMap.put(new StatusEventPair<>(sourceStatus, event), targetStatus);

}

/**

* 通过源状态和事件,获取目标状态

*/

public S getTargetStatus(S sourceStatus, E event) {

return statusEventMap.get(new StatusEventPair<>(sourceStatus, event));

}

}

定义支付状态机。注:支付、退款等不同的业务状态机是独立的。

/**

* 支付状态机

*/

public enum PaymentStatus implements BaseStatus {

INIT("INIT", "初始化"),

PAYING("PAYING", "支付中"),

PAID("PAID", "支付成功"),

FAILED("FAILED", "支付失败"),

;

// 支付状态机内容

private static final StateMachine STATE_MACHINE = new StateMachine<>();

static {

// 初始状态

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

}

// 状态

private final String status;

// 描述

private final String description;

PaymentStatus(String status, String description) {

this.status = status;

this.description = description;

}

/**

* 通过源状态和事件类型获取目标状态

*/

public static PaymentStatus getTargetStatus(PaymentStatus sourceStatus, PaymentEvent event) {

return STATE_MACHINE.getTargetStatus(sourceStatus, event);

}

}

定义支付事件。注:支付、退款等不同业务的事件是不一样的。

/**

* 支付事件

*/

public enum PaymentEvent implements BaseEvent {

// 支付创建

PAY_CREATE("PAY_CREATE", "支付创建"),

// 支付中

PAY_PROCESS("PAY_PROCESS", "支付中"),

// 支付成功

PAY_SUCCESS("PAY_SUCCESS", "支付成功"),

// 支付失败

PAY_FAIL("PAY_FAIL", "支付失败");

/**

* 事件

*/

private String event;

/**

* 事件描述

*/

private String description;

PaymentEvent(String event, String description) {

this.event = event;

this.description = description;

}

}

在支付单模型中声明状态和根据事件推进状态的方法:

/**

* 支付单模型

*/

public class PaymentModel {

/**

* 其它所有字段省略

*/

// 上次状态

private PaymentStatus lastStatus;

// 当前状态

private PaymentStatus currentStatus;

/**

* 根据事件推进状态

*/

public void transferStatusByEvent(PaymentEvent event) {

// 根据当前状态和事件,去获取目标状态

PaymentStatus targetStatus = PaymentStatus.getTargetStatus(currentStatus, event);

// 如果目标状态不为空,说明是可以推进的

if (targetStatus != null) {

lastStatus = currentStatus;

currentStatus = targetStatus;

} else {

// 目标状态为空,说明是非法推进,进入异常处理,这里只是抛出去,由调用者去具体处理

throw new StateMachineException(currentStatus, event, "状态转换失败");

}

}

}

代码注释已经写得很清楚,其中StateMachineException是自定义,不想定义的话,直接使用RuntimeException也是可以的。

在支付业务代码中的使用:只需要paymentModel.transferStatusByEvent(PaymentEvent.valueOf(message.getEvent()))

/**

* 支付领域域服务

*/

public class PaymentDomainServiceImpl implements PaymentDomainService {

/**

* 支付结果通知

*/

public void notify(PaymentNotifyMessage message) {

PaymentModel paymentModel = loadPaymentModel(message.getPaymentId());

try {

// 状态推进

paymentModel.transferStatusByEvent(PaymentEvent.valueOf(message.getEvent()));

savePaymentModel(paymentModel);

// 其它业务处理

... ...

} catch (StateMachineException e) {

// 异常处理

... ...

} catch (Exception e) {

// 异常处理

... ...

}

}

}

上面的代码只需要加完善异常处理,优化一下注释,就可以直接用起来。

上面写法的好处:

定义了明确的状态、事件。状态机的推进,只能通过“当前状态、事件、目标状态”来推进,不能通过if else 或case switch来直接写。比如:STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);避免终态变更。比如线上碰到if else写状态机,渠道异步通知比同步返回还快,异步通知回来把订单更新为“PAIED”,然后同步返回的代码把单据重新推进到PAYING。

2.4 Golang版本实现

项目结构:

①定义基础状态机: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

}

②定义支付状态机: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() {

//支付状态机初始化,包含所有可能的情况

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("获取目标状态失败")

}

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("状态转换失败")

}

}

③使用及测试

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....")

//调用第三方支付接口和其他业务处理逻辑

time.Sleep(time.Second * 15)

log.Infof("done...")

testOrder.TransferStatusByEvent(model.PAY_SUCCESS)

}

func getOrderStatus(context *context.Context) {

context.WriteString(string(testOrder.CurrentStatus))

}

声明:为了快速验证以及让代码更加简洁,没有按照标准的规范来编写controller、service、dao等。

测试:

启动程序,调用create接口,创建订单

http://localhost:8899/order/create

调用支付接口支付订单

http://localhost:8899/order/pay

我们手动模拟调用第三方支付接口,sleep了几十秒(实际调用肯定比这个快多了),所以不会立即返回结果,我们需要新开一个窗口,直接查询订单状态

立即调用查询接口获取订单状态,查看是否为支付中

http://localhost:8899/order/status

等待支付成功后,调用接口查看订单状态,是否为已支付

等待后台日志打印done之后重新调用查询接口:

http://localhost:8899/order/status

3 并发更新问题:多线程修改同一状态机(db版本号)

“状态机领域模型同时被两个线程操作怎么避免状态幂等问题?” 这是一个好问题。在分布式场景下,这种情况太过于常见。同一机器有可能多个线程处理同一笔业务,不同机器也可能处理同一笔业务。

业内通常的做法是设计良好的状态机 + 数据库锁 + 数据版本号解决。

简要说明:

状态机一定要设计好,只有特定的原始状态 + 特定的事件才可以推进到指定的状态。比如 INIT + 支付成功才能推进到sucess。更新数据库之前,先使用select for update进行锁行记录,同时在更新时判断版本号是否是之前取出来的版本号,更新成功就结束,更新失败就组成消息发到消息队列,后面再消费。通过补偿机制兜底,比如查询补单。

通过上述三个步骤,正常情况下,最终的数据状态一定是正确的。除非是某个系统有异常,比如外部渠道开始返回支付成功,然后又返回支付失败,说明依赖的外部系统已经异常,这样只能进人工差错处理流程。

参考文章:https://juejin.cn/post/7321569896453521419

参考阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: