全网首发:Seata Saga状态机设计器实战(上)

简介: 全网首发:Seata Saga状态机设计器实战(上)

前言


目前业界公认 Saga 是作为长事务的解决方案。而seata作为目前最流行的分布式事物解决方案也提供了Saga的支持。而采用Seata的Saga模式进行事物控制,核心就是通过状态机来进行控制,本文重点介绍Seata Saga状态机设计器的使用实战。


一、为什么要研究Seata Saga状态机设计器?


saga提供了两种实现方式,一种是编排,另一种是控制。seata的实现方式是后者。seata的控制器使用状态机驱动事务执行。

同AT模式,在saga模式下,seata也提供了RM、TM和TC三个角色。TC也是位于sever端,RM和TM位于客户端。TM用于开启全局事务,RM开启分支事务,TC监控事务运行。


Seata Saga 采用了状态机+DSL 方案来实现,原因有以下几个:


状态机+DSL 方案在实际生产中应用更广泛;

可以使用 Actor 模型或 SEDA 架构等异步处理引擎来执行,提高整体吞吐量;

通常在核心系统以上层的业务系统会伴随有“服务编排”的需求,而服务编排又有事务最终一致性要求,两者很难分割开,状态机+DSL 方案可以同时满足这两个需求;


由于 Saga 模式在理论上是不保证隔离性的,在极端情况下可能由于脏写无法完成回滚操作,比如举一个极端的例子, 分布式事务内先给用户 A 充值,然后给用户 B 扣减余额,如果在给 A 用户充值成功,在事务提交以前,A 用户把线消费掉了,如果事务发生回滚,这时则没有办法进行补偿了,有些业务场景可以允许让业务最终成功,在回滚不了的情况下可以继续重试完成后面的流程,状态机+DSL 的方案可以实现“向前”恢复上下文继续执行的能力, 让业务最终执行成功,达到最终一致性的目的。


在不保证隔离性的情况下:业务流程设计时要遵循“宁可长款, 不可短款”的原则,长款意思是客户少了线机构多了钱,以机构信誉可以给客户退款,反之则是短款,少的线可能追不回来了。所以在业务流程设计上一定是先扣款。


二、Seata Saga状态机设计器实战


在使用saga模式前,我们需要先定义好状态机,seata提供了网址可以可视化编辑状态机:

http://seata.io/saga_designer/index.html

网上很多地方说通过编辑器编辑后导出的状态机json无法使用,还需要手动编辑。而官网又导出的json可以直接使用,引擎能自动解析,但是又没有给出一个可以参考允许的json。

seata本来是非常优秀的框架,但是真的感觉毁在了文档上,可参考运行的demo实在有限。


在saga模式下,一个状态机实例就是一个全局事务,状态机中的每个状态是分支事务。


1、状态机流程图

下面给出一个可运行实例:

27.png

说明:

业务逻辑还是使用官网的例子,InventoryAction是库存服务,BalanceAction是账户服务。两个参与都有一个reduce方法,表示库存扣减或余额扣减,还有一个compensateReduce方法,表示补偿扣减操作。


2、状态机完整json

对应的完整状态机json:

{
  "nodes": [
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "#FA8C16",
      "label": "Start",
      "stateId": "Start",
      "stateType": "Start",
      "stateProps": {
        "StateMachine": {
          "Name": "reduceInventoryAndBalance",
          "Comment": "reduce inventory then reduce balance in a transaction",
          "Version": "0.0.1"
        },
        "Next": "InventoryAction"
      },
      "x": 467.875,
      "y": 53,
      "id": "973bd79e",
      "index": 9
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-rect",
      "color": "#1890FF",
      "label": "InventoryAction",
      "stateId": "InventoryAction",
      "stateType": "ServiceTask",
      "stateProps": {
        "Type": "ServiceTask",
        "ServiceName": "inventoryAction",
        "Next": "ChoiceState",
        "ServiceMethod": "reduce",
        "Input": [
          "$.[businessKey]",
          "$.[count]"
        ],
        "Output": {
          "reduceInventoryResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "CompensateState": "CompensateReduceInventory",
        "Retry": []
      },
      "x": 467.875,
      "y": 172,
      "id": "e17372e4",
      "index": 10
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-rect",
      "color": "#1890FF",
      "label": "ReduceBalance",
      "stateId": "ReduceBalance",
      "stateType": "ServiceTask",
      "stateProps": {
        "Type": "ServiceTask",
        "ServiceName": "balanceAction",
        "ServiceMethod": "reduce",
        "CompensateState": "CompensateReduceBalance",
        "Input": [
          "$.[businessKey]",
          "$.[amount]",
          {
            "throwException": "$.[mockReduceBalanceFail]"
          }
        ],
        "Output": {
          "compensateReduceBalanceResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Next": "Succeed"
      },
      "x": 467.125,
      "y": 411,
      "id": "a6c40952",
      "index": 11
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-capsule",
      "color": "#722ED1",
      "label": "CompensateReduceInventory",
      "stateId": "CompensateReduceInventory",
      "stateType": "Compensation",
      "stateProps": {
        "Type": "Compensation",
        "ServiceName": "inventoryAction",
        "ServiceMethod": "compensateReduce",
        "Input": [
          "$.[businessKey]"
        ]
      },
      "x": 264.875,
      "y": 171,
      "id": "3b348652",
      "index": 12
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-capsule",
      "color": "#722ED1",
      "label": "CompensateReduceBalance",
      "stateId": "CompensateReduceBalance",
      "stateType": "Compensation",
      "stateProps": {
        "Type": "Compensation",
        "ServiceName": "inventoryAction",
        "ServiceMethod": "compensateReduce",
        "Input": [
          "$.[businessKey]"
        ]
      },
      "x": 262.125,
      "y": 411,
      "id": "13b600b1",
      "index": 13
    },
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "#05A465",
      "label": "Succeed",
      "stateId": "Succeed",
      "stateType": "Succeed",
      "x": 468.125,
      "y": 568,
      "id": "690e5c5e",
      "stateProps": {
        "Type": "Succeed"
      },
      "index": 14
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-capsule",
      "color": "red",
      "label": "Compensation\nTrigger",
      "stateId": "CompensationTrigger",
      "stateType": "CompensationTrigger",
      "x": 783.625,
      "y": 417.5,
      "id": "757e057f",
      "stateProps": {
        "Type": "CompensationTrigger",
        "Next": "Fail"
      },
      "index": 15
    },
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "red",
      "label": "Fail",
      "stateId": "Fail",
      "stateType": "Fail",
      "stateProps": {
        "Type": "Fail",
        "ErrorCode": "PURCHASE_FAILED",
        "Message": "purchase failed"
      },
      "x": 783.625,
      "y": 286,
      "id": "0131fc0c",
      "index": 16
    },
    {
      "type": "node",
      "size": "80*72",
      "shape": "flow-rhombus",
      "color": "#13C2C2",
      "label": "Choice",
      "stateId": "ChoiceState",
      "stateType": "Choice",
      "x": 467.875,
      "y": 286.5,
      "id": "c11238b3",
      "stateProps": {
        "Type": "Choice",
        "Choices": [
          {
            "Expression": "[reduceInventoryResult] == true",
            "Next": "ReduceBalance"
          }
        ],
        "Default": "Fail"
      }
    },
    {
      "type": "node",
      "size": "39*39",
      "shape": "flow-circle",
      "color": "red",
      "label": "Catch",
      "stateId": "Catch",
      "stateType": "Catch",
      "x": 522.625,
      "y": 430.5,
      "id": "053ac3ac"
    }
  ],
  "edges": [
    {
      "source": "973bd79e",
      "sourceAnchor": 2,
      "target": "e17372e4",
      "targetAnchor": 0,
      "id": "f0a9008f",
      "index": 1
    },
    {
      "source": "a6c40952",
      "sourceAnchor": 2,
      "target": "690e5c5e",
      "targetAnchor": 0,
      "id": "da5a6275",
      "index": 2
    },
    {
      "source": "e17372e4",
      "sourceAnchor": 3,
      "target": "3b348652",
      "targetAnchor": 1,
      "id": "52a2256e",
      "style": {
        "lineDash": "4"
      },
      "index": 3
    },
    {
      "source": "a6c40952",
      "sourceAnchor": 3,
      "target": "13b600b1",
      "targetAnchor": 1,
      "id": "474512d9",
      "style": {
        "lineDash": "4"
      },
      "index": 4
    },
    {
      "source": "757e057f",
      "sourceAnchor": 0,
      "target": "0131fc0c",
      "targetAnchor": 2,
      "id": "1abf48fa",
      "index": 5
    },
    {
      "source": "e17372e4",
      "sourceAnchor": 2,
      "target": "c11238b3",
      "targetAnchor": 0,
      "id": "cd8c3104"
    },
    {
      "source": "c11238b3",
      "sourceAnchor": 2,
      "target": "a6c40952",
      "targetAnchor": 0,
      "id": "e47e49bc",
      "stateProps": {},
      "label": "",
      "shape": "flow-smooth"
    },
    {
      "source": "c11238b3",
      "sourceAnchor": 1,
      "target": "0131fc0c",
      "targetAnchor": 3,
      "id": "e3f9e775",
      "stateProps": {},
      "label": "",
      "shape": "flow-smooth"
    },
    {
      "source": "053ac3ac",
      "sourceAnchor": 1,
      "target": "757e057f",
      "targetAnchor": 3,
      "id": "3f7fe6ad",
      "stateProps": {
        "Exceptions": [
          "java.lang.Throwable"
        ],
        "Next": "CompensationTrigger"
      },
      "label": "",
      "shape": "flow-smooth"
    }
  ]
}


3、细节说明


3.1 开始节点Start

26.png

说明:

StateMachine下配置的是"状态机" 属性:


Name: 表示状态机的名称,必须唯一

Comment: 状态机的描述

Version: 状态机定义版本

Next指定下一个节点的ID。


3.2 任务节点ServiceTask

25.png

完整Props内容:

{
  "Type": "ServiceTask",
  "ServiceName": "inventoryAction",
  "Next": "ChoiceState",
  "ServiceMethod": "reduce",
  "Input": [
    "$.[businessKey]",
    "$.[count]"
  ],
  "Output": {
    "reduceInventoryResult": "$.#root"
  },
  "Status": {
    "#root == true": "SU",
    "#root == false": "FA",
    "$Exception{java.lang.Throwable}": "UN"
  },
  "CompensateState": "CompensateReduceInventory",
  "Retry": []
}


说明:

1、需要采用Next配置指定状态机下一个节点。

2、ServiceTask节点必须配置对应的状态补偿节点CompensateState

3、Input: 调用服务的输入参数列表, 是一个数组, 对应于服务方法的参数列表, 通过金钱符号获取参数


$.表示使用表达式从状态机上下文中取参数


表达使用的SpringEL, 如果是常量直接写值即可。

4、Output: 将服务返回的参数赋值到状态机上下文中, 是一个map结构,key为放入到状态机上文时的key(状态机上下文也是一个map),


value中$.是表示SpringEL表达式,表示从服务的返回参数中取值,#root表示服务的整个返回参数


5、Status: 服务执行状态映射,框架定义了三个状态,SU 成功、FA 失败、UN 未知, 我们需要把服务执行的状态映射成这三个状态,帮助框架判断整个事务的一致性,是一个map结构,key是条件表达式,一般是取服务的返回值或抛出的异常进行判断,默认是SpringEL表达式判断服务返回参数,带$Exception{开头表示判断异常类型。value是当这个条件表达式成立时则将服务执行状态映射成这个值


3.3 选择节点Choice

用来对状态机执行链路的多条件分支判断。

23.png

24.png


说明:

1、Choice类型的"状态"是单项选择路由 Choices: 可选的分支列表, 只会选择第一个条件成立的分支 Expression: SpringEL表达式 Next: 当Expression表达式成立时执行的下一个"状态"

2、Choices属性下可以配置多个判断分支

3、Default属性指定默认的下一个节点的ID

4、这里一定要注意,对Choice节点发散出去的带箭头线条上面的判定属性的配置。如果在Choices属性下已经将分支判定属性配置清楚,那么一定要将这些发散出去线条的默认属性置空,配置成{},否则会出现如下异常:

Caused by: java.lang.ClassCastException: java.lang.Boolean cannot be cast to java.lang.String


下面是choice线条默认的属性,如果不修改配置会出现异常。

22.png


3.4 异常捕捉节点Choice

20.png

21.png


说明:

1、异常捕捉节点要通过图形覆盖在状态任务节点的图形上,实现2者之间的关联。

比如上图中,Catch节点就是针对ReduceBalance节点进行异常捕获。

2、通过对Catch节点发散出去的箭头线上的属性配置,指定对什么异常进行捕获,以及捕获到对应的异常后下一个流转节点的名称。


3.4 成功节点Succeed

19.png


运行到"Succeed状态"表示状态机正常结束, 正常结束不代表成功结束, 是否成功要看每个"状态"是否都成功


3.5 失败节点Fail

18.png

运行到"Fail状态"状态机异常结束, 异常结束时可以配置ErrorCode和Message, 表示错误码和错误信息, 可以用于给调用方返回错误码和消息


3.6 补偿触发节点CompensationTrigger

17.png

CompensationTrigger类型的state是用于触发补偿事件, 回滚分布式事务 Next: 补偿成功后路由到的state。


目录
相关文章
|
9月前
|
存储 NoSQL Nacos
Seata分布式事务实战 2
Seata分布式事务实战
85 0
|
9月前
|
存储 SpringCloudAlibaba Java
Seata分布式事务实战 1
Seata分布式事务实战
95 0
|
30天前
|
Java 数据库连接 API
分布式事物【XA强一致性分布式事务实战、Seata提供XA模式实现分布式事务】(五)-全面详解(学习总结---从入门到深化)
分布式事物【XA强一致性分布式事务实战、Seata提供XA模式实现分布式事务】(五)-全面详解(学习总结---从入门到深化)
73 0
|
30天前
|
开发框架 Java 数据库连接
分布式事物【XA强一致性分布式事务实战、Seata提供XA模式实现分布式事务】(五)-全面详解(学习总结---从入门到深化)(下)
分布式事物【XA强一致性分布式事务实战、Seata提供XA模式实现分布式事务】(五)-全面详解(学习总结---从入门到深化)
51 0
|
10月前
|
SQL JSON Java
Seata分布式事务模式(TA、TCC、XA、SAGA)工作机制
分布式应用有一个比较明显的问题就是,一个业务流程通常需要几个服务来完成,业务的一致性很难保证。为了保障业务一致性,每一步都要在 catch 里去处理前面所有的“回滚”操作,可读性及维护性差,开发效率低下。
315 0
|
10月前
|
存储 SQL Java
|
11月前
|
安全 容灾 Nacos
Seata中的四种不同的事务模式之一SAGA模式
Saga 模式是 Seata 即将开源的长事务解决方案,由蚂蚁金服主要贡献
190 0
|
11月前
|
存储 SQL SpringCloudAlibaba
十一.SpringCloudAlibaba极简入门-分布式事务实战seata
在单体应用中通常情况下只有一个数据库(单数据源),集成事务是一个非常容易的工作。Spring对事务做了很好的管理,我们只需要通过简单的注解@Transactional就可以完成本地事务管理。 但是在微服务项目中事务的管理变得困难,因为微服务项目往往有很多的数据库组成,如果在一个业务中涉及到了对多个微服务以及多个数据库的写操作(跨多个数据源),那么要如何才能保证多个数据库组件的读写一致呢?即:同时操作两个数据库,数据库A写操作成功过,数据库B写操作失败要怎么样让数据库A的写操作回滚?很显然用本地事务管理是不能实现了。 我们知道,虽然Spring对事务做了很好的管理和封装,但是最终都是调用数据
|
11月前
|
SQL 前端开发 Java
有来实验室|第一篇:Seata1.5.2版本部署和开源全栈商城订单支付业务实战(二)
有来实验室|第一篇:Seata1.5.2版本部署和开源全栈商城订单支付业务实战(二)
|
11月前
|
NoSQL 关系型数据库 MySQL
有来实验室|第一篇:Seata1.5.2版本部署和开源全栈商城订单支付业务实战(一)
有来实验室|第一篇:Seata1.5.2版本部署和开源全栈商城订单支付业务实战(一)

热门文章

最新文章