5 张图带你了解分布式事务 Saga 模式中的状态机

大家好,我是君哥。

状态机在我们的工作中应用非常广泛,今天聊一聊分布式事务中间件 Seata 中 Saga 模式的状态机。

1 状态机简介

状态机是一个数学模型,它将工作中的运行状态和流转规则抽象出来,可以协调相关信号来完成预先设定的操作。

下面介绍状态机中的几个概念:

  • 状态:状态机目前的状态标识;

  • 状态转移:定义状态之间的转移路由;

  • 动作(Action):状态转移需要的操作;

  • 事件:要执行某个操作时的触发器或者口令。

状态机一般用在状态类型比较多(超过 3 个),分支流程比较多,初始状态经过多个流程的流转达到最终状态的场景。

2 Saga 模式

Saga 模式是分布式事务中长事务的一种解决方案,Seata 中 Saga 模式的理论基础是 Hector & Kenneth 在 1987 年发表的论文 Sagas。下图(来自官网)是 Seata 中 Saga 模型:

图片

在 Saga 模式中,如果一部分分支事务已经提交成功,当其中一个分支事务提交失败,状态机就会触发所有提交成功的分支事务进行回滚。

分支事务中提交和回滚的逻辑需要由业务代码来实现。

3 Saga 实现

Seata 中 Saga 模式是基于状态机来实现的,使用 Saga 模式时,先画一张状态图,这个状态图定义服务调用流程,每个节点调用一个分支事务,并且每个节点需要配备一个补偿节点用于分支事务失败后的补偿动作。

以经典电商案例来讲,一个分布式事务中有三个分支事务参数者:

分支事务动作状态
订单服务保存订单保存成功、失败
账户服务扣减金额扣减成功、失败
库存服务扣减库存扣减成功、失败

在这个分布式事务中,只有订单、账户、库存这三个分支事务都提交成功,整个事务才能成功。每一个分支事务提交失败,其他执行成功的事务都需要反向补偿。如下图:

图片

比如扣减金额这个分支事务失败了,需要反向补偿扣减金额、保存订单这两个分支事务。那 Seata 是怎么做到事件触发、状态流转和补偿操作的呢?

使用 Seata 状态机,首先需要定义一个 Json 文件,这个 Json 文件把图中的每个节点都定义成一个 State,State 的类型共有四种:

  • ServiceTask:对应分支事务的提交操作;

  • Choice:对应流程中下一个 State 的选择;

  • CompensationTrigger:触发补偿服务;

  • Succeed:成功状态,当所有分支事务都成功后才会流转到这个状态;

  • Fail:失败状态。

3.1 ServiceTask

下面我们看"保存订单"这个状态:

"SaveOrder": {
 "Type": "ServiceTask",
 "ServiceName": "orderSave",
 "ServiceMethod": "saveOrder",
 "CompensateState": "DeleteOrder",
 "Next": "ChoiceAccountState",
 "Input": [
  "$.[businessKey]",
  "$.[order]"
 ],
 "Output": {
  "SaveOrderResult": "$.#root"
 },
 "Status": {
  "#root == true": "SU",
  "#root == false": "FA",
  "$Exception{java.lang.Throwable}": "UN"
 },
 "Catch": [
  {
   "Exceptions": [
    "java.lang.Throwable"
   ],
   "Next": "CompensationTrigger"
  }
 ]
},

这个 State 的类型是 ServiceTask,上面图中的分支服务和补偿服务都是这种类型,也对应代码中的一个 Service。上面的 Json 中主要定义了三个内容:

  • 这个 state 调用的 Service 方法;

  • 提交失败后的补偿 State(CompensateState);

  • 提交成功后应该跳转的下一个 State(ChoiceAccountState)。

3.2 Choice

下面来看 ChoiceAccountState 这个状态节点,Json 文件定义如下:

"ChoiceAccountState":{
 "Type": "Choice",
 "Choices":[
  {
   "Expression":"[SaveOrderResult] == true",
   "Next":"ReduceAccount"
  }
 ],
 "Default":"Fail"
}

对应的下个节点是 ReduceAccount,如果失败就会跳转 Fail 状态。

3.3 Fail

上面 orderSave 这个状态节点如果发生异常,会跳转到 CompensationTrigger,CompensationTrigger 状态节点定义如下:

"CompensationTrigger": {
 "Type": "CompensationTrigger",
 "Next": "Fail"
}

这个节点会触发 SaveOrder 中定义的补偿服务,然后将最终状态流转到 Fail。同时我们也看到,只要到了 CompensationTrigger 这个状态节点,最终状态就会流转到 Fail。

下面我们把整个 Json 文件的定义贴出来看一下:

{
    "Name": "buyGoodsOnline",
    "Comment": "buy a goods on line, add order, deduct account, deduct storage ",
    "StartState": "SaveOrder",
    "Version": "0.0.1",
 #定义状态
    "States": {
        "SaveOrder": {
            "Type": "ServiceTask",
            "ServiceName": "orderSave",
            "ServiceMethod": "saveOrder",
            "CompensateState": "DeleteOrder",
            "Next": "ChoiceAccountState",
            "Input": [
                "$.[businessKey]",
                "$.[order]"
            ],
            "Output": {
                "SaveOrderResult": "$.#root"
            },
            "Status": {
                "#root == true": "SU",
                "#root == false": "FA",
                "$Exception{java.lang.Throwable}": "UN"
            },
   "Catch": [
                {
                    "Exceptions": [
                        "java.lang.Throwable"
                    ],
                    "Next": "CompensationTrigger"
                }
            ]
        },
        "ChoiceAccountState":{
            "Type": "Choice",
            "Choices":[
                {
                    "Expression":"[SaveOrderResult] == true",
                    "Next":"ReduceAccount"
                }
            ],
            "Default":"Fail"
        },
        "ReduceAccount": {
            "Type": "ServiceTask",
            "ServiceName": "accountService",
            "ServiceMethod": "decrease",
            "CompensateState": "CompensateReduceAccount",
            "Next": "ChoiceStorageState",
            "Input": [
                "$.[businessKey]",
                "$.[userId]",
                "$.[money]",
                {
                    "throwException" : "$.[mockReduceAccountFail]"
                }
            ],
            "Output": {
                "ReduceAccountResult": "$.#root"
            },
            "Status": {
                "#root == true": "SU",
                "#root == false": "FA",
                "$Exception{java.lang.Throwable}": "UN"
            },
            "Catch": [
                {
                    "Exceptions": [
                        "java.lang.Throwable"
                    ],
                    "Next": "CompensationTrigger"
                }
            ]
        },
        "ChoiceStorageState":{
            "Type": "Choice",
            "Choices":[
                {
                    "Expression":"[ReduceAccountResult] == true",
                    "Next":"ReduceStorage"
                }
            ],
            "Default":"Fail"
        },
        "ReduceStorage": {
            "Type": "ServiceTask",
            "ServiceName": "storageService",
            "ServiceMethod": "decrease",
            "CompensateState": "CompensateReduceStorage",
            "Input": [
                "$.[businessKey]",
                "$.[productId]",
                "$.[count]",
                {
                    "throwException" : "$.[mockReduceStorageFail]"
                }
            ],
            "Output": {
                "ReduceStorageResult": "$.#root"
            },
            "Status": {
                "#root == true": "SU",
                "#root == false": "FA",
                "$Exception{java.lang.Throwable}": "UN"
            },
            "Catch": [
                {
                    "Exceptions": [
                        "java.lang.Throwable"
                    ],
                    "Next": "CompensationTrigger"
                }
            ],
            "Next": "Succeed"
        },
        "DeleteOrder": {
            "Type": "ServiceTask",
            "ServiceName": "orderSave",
            "ServiceMethod": "deleteOrder",
            "Input": [
                "$.[businessKey]",
                "$.[order]"
            ]
        },
        "CompensateReduceAccount": {
            "Type": "ServiceTask",
            "ServiceName": "accountService",
            "ServiceMethod": "compensateDecrease",
            "Input": [
                "$.[businessKey]",
                "$.[userId]",
                "$.[money]"
            ]
        },
        "CompensateReduceStorage": {
            "Type": "ServiceTask",
            "ServiceName": "storageService",
            "ServiceMethod": "compensateDecrease",
            "Input": [
                "$.[businessKey]",
                "$.[productId]",
                "$.[count]"
            ]
        },
        "CompensationTrigger": {
            "Type": "CompensationTrigger",
            "Next": "Fail"
        },
        "Succeed": {
            "Type":"Succeed"
        },
        "Fail": {
            "Type":"Fail",
            "ErrorCode": "PURCHASE_FAILED",
            "Message": "purchase failed"
        }
    }
}

上面 Json 文件中定义的 buyGoodsOnline,是状态机加载的入口,状态机会找到这个 name,然后把状态加载到自己的内存中。下面,我们再来总结一下电商案例中分布式事务状态流转过程:

图片

4 状态机应用

上面的电商例子中,三个分支服务分别定义了三个 State,对应的 ServiceMethod 如下:

  • SaveOrder#saveOrder:

public boolean saveOrder(String businessKey, Order order) {
 logger.info("保存订单, businessKey:{}, order: {}", businessKey, order);
 orderDao.create(order);
 return true;
}
  • ReduceAccount#decrease

public boolean decrease(String businessKey, Long userId, BigDecimal money) {
 return accountApi.decrease(businessKey, userId, money);
}
  • ReduceStorage#decrease

public boolean decrease(String businessKey, Long productId, Integer count) {
 return storageApi.decrease(businessKey, productId, count);
}

状态机在启动的时候,需要把上面方法中的参数都传入,实例代码如下:

StateMachineEngine stateMachineEngine = (StateMachineEngine) ApplicationContextUtils.getApplicationContext().getBean("stateMachineEngine");
Map<String, Object> startParams = new HashMap<>(3);
String businessKey = String.valueOf(System.currentTimeMillis());
startParams.put("businessKey", businessKey);
startParams.put("order", order);
startParams.put("mockReduceAccountFail", "true");
startParams.put("userId", order.getUserId());
startParams.put("money", order.getPayAmount());
startParams.put("productId", order.getProductId());
startParams.put("count", order.getCount());
//这里采用同步方法
StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("buyGoodsOnline", null, businessKey, startParams);

5 状态机原理

下面这张图来自于 Seata 官网,主要讲解了状态机的工作原理:

图片

  1. 状态机启动时,首先启动了全局事务;

  2. 将状态机的参数记录在本地 seata_state_machine_inst 表;

  3. 向 Seata Server 注册分支事务;

  4. 执行 StateA 并记录状态到本地数据库,同时会产生路由事件放入 EventQueue,执行 StateB 时取出路由消息触发执行。同样 StateB 执行时也会产生路由消息放入 EventQueue;

  5. 从 EventQueue 取出路由消息执行 StateC;

  6. 状态机结束流程,提交或回滚全局事务。

6 高可用

Seata 中的状态机并不是独立部署,而是内嵌在应用中,由于状态机上下文和执行日志都记录在本地数据库中,所以状态机本身是无状态的。

状态机启动时,会发送状态到 Seata Server,当一个应用宕机后,Seata Server 能感知到,并会把恢复请求发送到存活的实例,收到请求的实例从数据库取出状态机上下文和执行日志进行恢复。如下图:

图片

7 总结

本文讲解了分布式事务中间件 Seata 给 Saga 模式设计的状态机使用方式和原理。状态机在我们的日常工作中使用非常广泛,希望 Seata 的设计能对我们设计状态机提供思路和参考。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

构造-析构-拷贝构造-赋值运算符重载-const成员函数

1. 类的6个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。 空类中真的什么都没有吗&#xff1f;并不是&#xff0c;任何类在什么时候都不写时&#xff0c;编译器会自动生成以下6个成员函数。 默认成员函数&#xff1a;用户没有显式实现&#xff0c;编译器…

C++之deque与vector、list对比分析

一.deque讲解 对于vector和list&#xff0c;前一个是顺序表&#xff0c;后一个是带头双向循环链表&#xff0c;前面我们已经实现过&#xff0c;这里就不再讲解了&#xff0c;直接上deque了。 deque&#xff1a;双端队列 常见接口大家可以查看下面链接&#xff1a; deque - …

STM32第九节(中级篇):RCC(第一节)——时钟树讲解

目录 前言 STM32第九节&#xff08;中级篇&#xff09;&#xff1a;RCC——时钟树讲解 时钟树主系统时钟讲解 HSE时钟 HSI时钟 锁相环时钟 系统时钟 SW位控制 HCLK时钟 PCLKI时钟 PCLK2时钟 RTC时钟 MCO时钟输出 6.2.7时钟安全系统(CSS&#xff09; 小结 前言 从…

单链表操作

单链表操作 1. 链表的概念2. 链表的分类2.1.单向或者双向2.2 带头或者不带头2.3 循环或者非循环2.4 常用的链表 3. 单链表的实现3.1 单链表的打印3.2 单链表的头插3.3 单链表的尾插3.4 单链表的头删3.5 单链表的尾删3.6 单链表的查询3.7 在pos前插入数据3.8 在pos后插入数据3.9…

Linux——进程通信(一) 匿名管道

目录 前言 一、进程间通信 二、匿名管道的概念 三、匿名管道的代码实现 四、管道的四种情况 1.管道无数据&#xff0c;读端需等待 2.管道被写满&#xff0c;写端需等待 3.写端关闭&#xff0c;读端一直读取 4.读端关闭&#xff0c;写端一直写入 五、管道的特性 前言 …

不锈钢多功能电工剥线钳分线绕线剪线剥线钳剥线压线扒皮钳子

品牌&#xff1a;银隆 型号&#xff1a;089B绿色 材质&#xff1a;镍铬钢&#xff08;不锈钢&#xff09; 颜色分类&#xff1a;089B灰色,089B红色,089B绿色,089B黑色,089B橙色 功能齐集一身&#xff0c;一钳多用&#xff0c;多功能剥线钳。剥线&#xff0c;剪线&#xff…

Java-CAS 原理与 JUC 原子类

由于 JVM 的 synchronized 重量级锁涉及到操作系统&#xff08;如 Linux&#xff09; 内核态下的互斥锁&#xff08;Mutex&#xff09;的使用&#xff0c; 其线程阻塞和唤醒都涉及到进程在用户态和到内核态频繁切换&#xff0c; 导致重量级锁开销大、性能低。 而 JVM 的 synchr…

免费阅读篇 | 芒果YOLOv8改进114:上采样Dysample:顶会ICCV2023,轻量级图像增采样器,通过学习采样来学习上采样,计算资源需求小

&#x1f4a1;&#x1f680;&#x1f680;&#x1f680;本博客 改进源代码改进 适用于 YOLOv8 按步骤操作运行改进后的代码即可 该专栏完整目录链接&#xff1a; 芒果YOLOv8深度改进教程 &#x1f680;&#x1f680;&#x1f680; DySample是一个超轻量级和有效的动态上采样器…

DDos攻击如何被高防服务器有效防范?

德迅云安全-领先云安全服务与解决方案提供商 什么是DDos攻击&#xff1f; DDos攻击是一种网络攻击手段&#xff0c;旨在通过使目标系统的服务不可用或中断&#xff0c;导致无法正常使用网络服务。DDos攻击可以采取多种方式实施&#xff0c;包括洪水攻击、压力测试、UDP Flood…

HTML静态网页成品作业(HTML+CSS)——游戏战地介绍设计制作(4个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有4个页面。 二、作品演示 三、代…

关于PXIE3U18槽背板原理拓扑关系

如今IT行业日新月异&#xff0c;飞速发展&#xff0c;随之带来的是数据吞吐量的急剧升高。大数据&#xff0c;大存储将成为未来数据通信的主流&#xff0c;建立快速、大容量的数据传输通道将成为电子系统的关键。随着集成技术和互连技术的发展&#xff0c;新的串口技术&#xf…

【QT+QGIS跨平台编译】之七十七:【QGIS_Gui跨平台编译】—【错误处理:字符串错误】

文章目录 一、字符串错误二、处理方法三、涉及到的文件一、字符串错误 常量中有换行符错误:(也有const char * 到 LPCWSTR 转换的错误) 二、处理方法 需要把对应的文档用记事本打开,另存为 “带有BOM的UTF-8” 三、涉及到的文件 src\gui\qgsadvanceddigitizingdockwidge…

ClickHouse中的设置的分类

ClickHouse中的各种设置 ClickHouse中的设置有几百个&#xff0c;下面对这些设置做了一个简单的分类。

【Godot 4.2】常见几何图形、网格、刻度线点求取函数及原理总结

概述 本篇为ShapePoints静态函数库的补充和辅助文档。ShapePoints函数库是一个用于生成常见几何图形顶点数据&#xff08;PackedVector2Array&#xff09;的静态函数库。生成的数据可用于_draw和Line2D、Polygon2D等进行绘制和显示。因为不断地持续扩展&#xff0c;ShapePoint…

Orbit 使用指南 03 | 与刚体交互 | Isaac Sim | Omniverse

如是我闻&#xff1a; “在之前的指南中&#xff0c;我们讨论了独立脚本&#xff08; standalone script&#xff09;的基本工作原理以及如何在模拟器中生成不同的对象&#xff08;prims&#xff09;。在指南03中&#xff0c;我们将展示如何创建并与刚体进行交互。为此&#xf…

机器学习周记(第三十周:文献阅读-SageFormer)2024.3.11~2024.3.17

目录 摘要 ABSTRACT 1 论文信息 1.1 论文标题 1.2 论文摘要 1.3 论文背景 2 论文模型 2.1 问题描述 2.2 模型信息 2.2.1 Series-aware Global Tokens&#xff08;序列感知全局标记&#xff09; 2.2.2 Graph Structure Learning&#xff08;图结构学习&#xff09; …

大数据面试题之SQL题

大数据面试题之SQL题 1.有一个录取学生人数表&#xff0c;记录的是每年录取学生人数和入学学生的学制 以下是表结构&#xff1a; CREATE TABLE admit ( id int(11) NOT NULL AUTO_INCREMENT, year int(255) DEFAULT NULL COMMENT ‘入学年度’, num int(255) DEFAULT NULL COMM…

交流互动系统|基于springboot框架+ Mysql+Java+Tomcat的交流互动系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

【医学图像处理】ECAT和HRRT格式转nii格式【超简单】

之前从ADNI上下载PET数据的时候发现有许多数据的格式不是DICOM的而是ECAT或者是HRRT格式&#xff0c;这对原本就少的PET数据是血上加霜啊。 当然只使用DICOM格式的数据也会得到不少的数据&#xff0c;我一开始也是只使用DICOM格式的样本&#xff0c;后来为了得到更多的数据&a…

2024年值得创作者关注的十大AI动画创新平台

别提找大型工作室制作动画了。如今,AI平台让我们就可以轻松制作动画。从简单的文本生动画功能到复杂的角色动作,这些平台为各种类型的创作者提供了不同的功能。 AI已经有了长足的发展,现在它可以理解复杂的人类动作和艺术意图,将简单的输入转化成丰富而详细的动画。 下面…