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 的设计能对我们设计状态机提供思路和参考。

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Docker是一种流行的容器化技术,通过轻量级、隔离性强的容器来运行应用程序。下面我将通过十张图你深入理解Docker容器和镜像。 1. 第一张图展示了Docker容器和镜像的关系。镜像是Docker的基础组件,它是一个只读的模板,包含了运行应用程序所需的所有文件和配置。容器是从镜像创建的实例,它具有自己的文件系统、网络和进程空间。 2. 第二张图展示了Docker容器的隔离性。每个容器都有自己的文件系统,这意味着容器之间的文件互不干扰。此外,每个容器还有自己的网络和进程空间,使得容器之间的网络和进程相互隔离。 3. 第三张图展示了Docker镜像和容器的可移植性。镜像可以在不同的主机上运行,只需在目标主机上安装Docker引擎即可。容器也可以很容易地在不同的主机上迁移,只需将镜像传输到目标主机并在其上创建容器。 4. 第四张图展示了Docker容器的快速启动。由于Docker容器与主机共享操作系统内核,启动容器只需几秒钟的时间。这使得快速部署和扩展应用程序成为可能。 5. 第五张图展示了Docker容器的可重复性。通过使用Dockerfile定义镜像构建规则,可以确保每次构建的镜像都是相同的。这样,可以消除由于环境差异导致的应用程序运行问题。 6. 第六张图展示了Docker容器的资源隔离性。Docker引擎可以为每个容器分配一定数量的CPU、内存和磁盘空间,确保容器之间的资源不会互相干扰。 7. 第七张图展示了Docker容器的可扩展性。通过使用Docker Swarm或Kubernetes等容器编排工具,可以在多个主机上运行和管理大规模的容器群集。 8. 第八张图展示了Docker镜像的分层结构。镜像由多个只读层组成,每个层都包含一个或多个文件。这种分层结构使得镜像的存储和传输变得高效。 9. 第九张图展示了Docker容器的生命周期。容器可以通过创建、启动、停止和销毁等命令来管理。这使得容器的维护和管理变得简单。 10. 第十张图展示了Docker容器的应用场景。Docker容器广泛应用于开发、测试、部署和运维等领域。它可以提供一致的开发和运行环境,简化了应用程序的管理和交付过程。 通过这十张图,希望能让大家更深入地理解Docker容器和镜像的概念、特性和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

君哥聊技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值