支付系统核心逻辑 — — 状态机(JavaGolang版本)

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

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

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

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

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

2 实战:支付核心逻辑

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

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

2.2 状态机设计原则

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

  1. 明确性:状态和转换必须清晰定义,避免含糊不清的状态。
  2. 完备性:为所有可能的事件-状态组合定义转换逻辑。
  3. 可预测性:系统应根据当前状态和给定事件可预测地响应。
  4. 最小化:状态数应保持最小,避免不必要的复杂性。
①明确性:状态与转换必须定义清晰
②完备性:需要考虑所有事件-状态的转换组合
③可预测性:需根据当前状态+给定事件可预测响应
④最小化:状态数要少,避免过于复杂
常见误区
  1. 过度设计:引入不必要的状态
  2. 不完备的处理:没有考虑到状态与事件所有可能的转换关系,导致系统行为不确定
  3. 硬编码逻辑:过多硬编码转换逻辑,导致系统不具备可扩展性和灵活性

比如下面的设计:

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

在这里插入图片描述

不合理的地方:

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

改进方案:

  • 删除不必要的状态。如:ACCEPT
  • 将一个大型状态机抽取为多份小的状态机。比如把一些退款REFUND、请款等单据单独抽取出来。这个样子,虽然状态机数量多了,但是每个状态机都更加清晰明了。
  1. 主单:
    在这里插入图片描述
  1. 普通支付单
    在这里插入图片描述
  1. 预授权单
    在这里插入图片描述
  1. 请款单
    在这里插入图片描述
  1. 退款单
    在这里插入图片描述
最佳实践及代码规范

代码层面:

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

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

2.3 Java版本实现

  1. 定义状态基类
/*** 状态基类*/
public interface BaseStatus {
}
  1. 定义事件基类
/*** 事件基类*/
public interface BaseEvent {
}
  1. 定义状态-事件对,指定的状态只能接受指定的事件
/*** 状态事件对,指定的状态只能接受指定的事件*/
public class StatusEventPair<S 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) {StatusEventPair<S, E> other = (StatusEventPair<S, 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);}
}
  1. 定义状态机
/*** 状态机*/
public class StateMachine<S extends BaseStatus, E extends BaseEvent> {private final Map<StatusEventPair<S, 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));}
}
  1. 定义支付状态机。注:支付、退款等不同的业务状态机是独立的。
/*** 支付状态机*/
public enum PaymentStatus implements BaseStatus {INIT("INIT", "初始化"),PAYING("PAYING", "支付中"),PAID("PAID", "支付成功"),FAILED("FAILED", "支付失败"),;// 支付状态机内容private static final StateMachine<PaymentStatus, 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);}
}
  1. 定义支付事件。注:支付、退款等不同业务的事件是不一样的。
/*** 支付事件*/
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;}
}
  1. 在支付单模型中声明状态和根据事件推进状态的方法:
/*** 支付单模型*/
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) {// 异常处理... ...}}
}

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

上面写法的好处:

  1. 定义了明确的状态、事件。
  2. 状态机的推进,只能通过“当前状态、事件、目标状态”来推进,不能通过if else 或case switch来直接写。比如:STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);
  3. 避免终态变更。比如线上碰到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 = "INIT"PAYING PaymentStatus = "PAYING"PAID   PaymentStatus = "PAID"FAILED PaymentStatus = "FAILED"
)type PaymentEvent stringconst (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    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/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.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等。

测试:

  1. 启动程序,调用create接口,创建订单
http://localhost:8899/order/create

在这里插入图片描述

  1. 调用支付接口支付订单
http://localhost:8899/order/pay

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

在这里插入图片描述

  1. 立即调用查询接口获取订单状态,查看是否为支付中
http://localhost:8899/order/status

在这里插入图片描述

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

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

在这里插入图片描述

http://localhost:8899/order/status

在这里插入图片描述

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

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

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

在这里插入图片描述
简要说明:

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/313261.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UE5 HLSL 详细学习笔记

这里的POSITION是变量Position的语义&#xff0c;告诉寄存器&#xff0c;此变量的保存位置&#xff0c;通常语义用于着色器的输入和输出&#xff0c;以冒号“&#xff1a;”的方式进一步说明此变量&#xff0c;COLOR也类似 还有什么语义呢&#xff1f; HLSL核心函数&#xff1a…

云服务器安装Mysql、MariaDB、Redis、tomcat、nginx

前置工作 进入根目录 cd / 都在/usr/local/src文件夹&#xff09; 上传压缩包 rz 压缩包 Mysql 1.下载并安装MySQL官方的 Yum Repository wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm rpm -ivh mysql-community-release-el7-5.noarch.rpm yum…

每日算法4/17

1552. 两球之间的磁力 题目 在代号为 C-137 的地球上&#xff0c;Rick 发现如果他将两个球放在他新发明的篮子里&#xff0c;它们之间会形成特殊形式的磁力。Rick 有 n 个空的篮子&#xff0c;第 i 个篮子的位置在 position[i] &#xff0c;Morty 想把 m 个球放到这些篮子里&…

工业数学模型——高炉煤气发生量预测(三)

1、工业场景 冶金过程中生产的各种煤气&#xff0c;例如高炉煤气、焦炉煤气、转炉煤气等。作为重要的副产品和二次能源&#xff0c;保证它们的梯级利用和减少放散是煤气能源平衡调控的一项紧迫任务&#xff0c;准确的预测煤气的发生量是实现煤气系统在线最优调控的前提。 2、…

A Geolocation Databases Study(2011年)第二部分:Geolocation Services

下载地址:A Geolocation Databases Study | IEEE Journals & Magazine | IEEE Xplore 被引次数:195 Shavitt Y, Zilberman N. A geolocation databases study[J]. IEEE Journal on Selected Areas in Communications, 2011, 29(10): 2044-2056. 2. Geolocation Services…

2024认证杯数学建模C题思路模型代码

目录 2024认证杯数学建模C题思路模型代码&#xff1a;4.11开赛后第一时间更新&#xff0c;获取见文末名片 以下为2023年认证杯C题&#xff1a; 2024年认证杯数学建模C题思路模型代码见此 2024认证杯数学建模C题思路模型代码&#xff1a;4.11开赛后第一时间更新&#xff0c;获…

一文掌握:图片转Base64编码的原理、实践(自定义图片本地缓存等)以及优化事项

图片转Base64是指将一幅图片&#xff08;如PNG、JPEG、GIF等格式&#xff09;的二进制数据编码为符合Base64规范的文本字符串的过程。图片Base64编码将图片数据转换为ASCII字符串&#xff0c;便于网络传输和存储。实现步骤包括读取图片文件、转换为字节数组&#xff0c;再通过编…

Windows 安装 A UDP/TCP Assistant 网络调试助手

Windows 安装 A UDP/TCP Assistant 网络调试助手 0. 引言1. 下载地址2. 安装和使用 0. 引言 需要调试一个实时在线聊天程序&#xff0c;安装一个UDP/TCP Assistant 网络调试助手&#xff0c;方便调试。 1. 下载地址 https://github.com/busyluo/NetAssistant/releases 2. 安…

Vue3项目 网易严选_学习笔记

Vue3项目 网易严选_第一天 主要内容 项目搭建vuex基础路由设计首页顶部和底部布局 学习目标 知识点要求项目搭建掌握vuex基础掌握路由设计掌握首页顶部和底部布局掌握 一、项目搭建 1.1 创建项目 vue create vue-wangyi选择vue3.0版本 1.2 目录调整 大致步骤&#xff…

美格智能出席紫光展锐第三届泛金融支付生态论坛,引领智慧金融变革向新

4月16日&#xff0c;以“融智创新&#xff0c;共塑支付产业新生态”为主题的紫光展锐第三届泛金融支付生态论坛在福州举办&#xff0c;来自金融服务机构、分析师机构、终端厂商、模组厂商等行业各领域生态伙伴汇聚一堂&#xff0c;探讨金融支付产业的机遇与挑战。作为紫光展锐重…

个人网站制作 Part 24 添加用户反馈功能[Userback] | Web开发项目添加页面缓存

文章目录 &#x1f469;‍&#x1f4bb; 基础Web开发练手项目系列&#xff1a;个人网站制作&#x1f680; 添加用户反馈功能&#x1f528;使用反馈工具&#x1f527;步骤 1: 选择反馈工具&#x1f527;步骤 2: 注册Userback账户&#x1f527;步骤 3: 获取反馈按钮代码 使用Vue.…

生成对抗网络GAN的扩展应用理解

注&#xff1a;本文仅个人学习理解&#xff0c;如需详细内容请看原论文&#xff01; 一、cycleGAN 1.整体架构&#xff1a; 将图片A通过生成器生成图片B&#xff0c;然后通过判别器判别图片B的真假&#xff1b;并且图片B通过另一个生成器生成A‘&#xff0c;让A和A’近似。 2…

Linux 下的文件夹对比工具 vddiff 介绍

大家好&#xff0c;我是孔令飞&#xff0c;字节跳动云原生开发专家、前腾讯云原生技术专家&#xff1b;《企业级Go项目开发实战》作者&#xff0c;云原生实战营 知识星球星主。欢迎关注我的公众号【令飞编程】&#xff0c;干货不错过。 在大家的日常工作中有没有遇到以下场景&a…

初始Next.js

版本&#xff1a; 本系列next.js基于的是目前最新版本的 v14 版本&#xff0c;需要 Node.js 18.17 及以后版本 创建项目&#xff1a; 最快捷的创建 Next.js 项目的方式是使用 create-next-app脚手架&#xff0c;你只需要运行&#xff1a; npx create-next-applatest&&am…

FinalShell 远程连接 Linux(Ubuntu)系统

Linux 系列教程&#xff1a; VMware 安装配置 Ubuntu&#xff08;最新版、超详细&#xff09;FinalShell 远程连接 Linux&#xff08;Ubuntu&#xff09;系统Ubuntu 系统安装 VS Code 并配置 C 环境 ➡️➡️➡️提出一个问题&#xff1a;为什么使用 FinalShell 连接&#xff0…

Python-VBA函数之旅-filter函数

目录 一、filter函数的常见应用场景&#xff1a; 二、filter函数的使用注意事项&#xff1a; 1、filter函数&#xff1a; 1-1、Python&#xff1a; 1-2、VBA&#xff1a; 2、相关文章&#xff1a; 个人主页&#xff1a;非风V非雨-CSDN博客 一、filter函数的常见应用…

FY-SA-20237·8-ElectricHealing

Translated from the Scientific American, July/August 2023 issue. Electric Healing (电疗) New bandage zaps and medicates chronic wounds 新型创可贴治疗慢性伤口 Paragraph 1 Some wounds won’t heal by themselves. 翻译&#xff1a;一些伤口不会自愈。 解释&…

phpMyadmin 设置显示完整内容

额外选项这里&#xff0c;默认部分内容改成完整内容 方案&#xff1a; 版本>4.5.4.1&#xff0c;修改文件&#xff1a;config.inc.php&#xff0c;添加一行代码&#xff1a; if ( !isset($_REQUEST[pftext])) $_REQUEST[pftext] F;

双向链表详解

目录 带头双向循环链表带头双向循环链表的实现带头双向循环链表的功能实现创造新节点LTNode* CreateLTNode(LTDataType x)代码 初始化链表LTNode*LTInit(LTNode* phead)代码 打印链表void LTPrint(LTNode* phead)代码 链表尾插void LTPushBack(LTNode* phead, LTDataType x)代码…

OpenHarmony 资源调度之内存管理源码分析

作者&#xff1a;张守忠 1 内存管理简介 内存管理部件位于全局资源调度管控子系统中&#xff0c;基于应用的生命周期状态&#xff0c;更新进程回收优先级列表&#xff0c;通过内存回收、查杀等手段管理系统内存&#xff0c;保障内存供给。 1.1 内存管理框架 内存管理部件主要…