专门给小公司做网站,做网站的像素,s001网站建设,iis6.0做网站压缩支付系统核心逻辑 — — 状态机 代码地址#xff1a;https://github.com/ziyifast/ziyifast-code_instruction/tree/main/state_machine_demo 1 概念#xff1a;FSM#xff08;有限状态机#xff09;#xff0c;模式之间转换 状态机#xff0c;也叫有限状态机#xff08…支付系统核心逻辑 — — 状态机 代码地址https://github.com/ziyifast/ziyifast-code_instruction/tree/main/state_machine_demo 1 概念FSM有限状态机模式之间转换 状态机也叫有限状态机FSMFinite 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 StatusEventPairS extends BaseStatus, E extends BaseEvent {/*** 指定的状态*/private final S status;/*** 可接受的事件*/private final E event;public StatusEventPair(S status, E event) {this.status status;this.event event;}Overridepublic boolean equals(Object obj) {if (obj instanceof StatusEventPair) {StatusEventPairS, E other (StatusEventPairS, E)obj;return this.status.equals(other.status) this.event.equals(other.event);}return false;}Overridepublic int hashCode() {// 这里使用的是google的guava包。com.google.common.base.Objectsreturn Objects.hashCode(status, event);}
}定义状态机
/*** 状态机*/
public class StateMachineS extends BaseStatus, E extends BaseEvent {private final MapStatusEventPairS, E, 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 StateMachinePaymentStatus, PaymentEvent 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 modeltype BaseStatus interface {
}type BaseEvent interface {
}type StatusEventPair struct {status BaseStatusevent 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 modeltype PaymentStatus stringconst (INIT PaymentStatus INITPAYING PaymentStatus PAYINGPAID PaymentStatus PAIDFAILED PaymentStatus FAILED
)type PaymentEvent stringconst (PAY_CREATE PaymentEvent PAY_CREATEPAY_PROCESS PaymentEvent PAY_PROCESSPAY_SUCCESS PaymentEvent PAY_SUCCESSPAY_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 PaymentStatusCurrentStatus PaymentStatus
}func (pm *PaymentModel) TransferStatusByEvent(event PaymentEvent) {targetStatus : GetTargetStatus(pm.CurrentStatus, event)if targetStatus ! {pm.lastStatus pm.CurrentStatuspm.CurrentStatus targetStatus} else {// 处理异常panic(状态转换失败)}
}③使用及测试
main.go
package mainimport (github.com/kataras/iris/v12github.com/kataras/iris/v12/contextgithub.com/ziyifast/logmyTest/demo_home/state_machine_demo/modeltime
)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.INITcontext.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/status3 并发更新问题多线程修改同一状态机db版本号 “状态机领域模型同时被两个线程操作怎么避免状态幂等问题” 这是一个好问题。在分布式场景下这种情况太过于常见。同一机器有可能多个线程处理同一笔业务不同机器也可能处理同一笔业务。 业内通常的做法是设计良好的状态机 数据库锁 数据版本号解决。 简要说明
状态机一定要设计好只有特定的原始状态 特定的事件才可以推进到指定的状态。比如 INIT 支付成功才能推进到sucess。更新数据库之前先使用select for update进行锁行记录同时在更新时判断版本号是否是之前取出来的版本号更新成功就结束更新失败就组成消息发到消息队列后面再消费。通过补偿机制兜底比如查询补单。
通过上述三个步骤正常情况下最终的数据状态一定是正确的。除非是某个系统有异常比如外部渠道开始返回支付成功然后又返回支付失败说明依赖的外部系统已经异常这样只能进人工差错处理流程。
参考文章https://juejin.cn/post/7321569896453521419