Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57

一、前言

通过以下系列章节:

docker-compose 实现Seata Server高可用部署 | Spring Cloud 51

Seata AT 模式理论学习、事务隔离及部分源码解析 | Spring Cloud 52

Spring Boot集成Seata利用AT模式分布式事务示例 | Spring Cloud 53

Seata XA 模式理论学习、使用及注意事项 | Spring Cloud54

Seata TCC 模式理论学习、生产级使用示例搭建及注意事项 | Spring Cloud55

Seata TCC 模式下解决幂等、悬挂、空回滚问题 | Spring Cloud56

我们对Seata及其ATXATCC事务模式的理论、使用有了深入的了解,今天继续学习SeataSaga事务模式进行学习;并区别与官网,我们利用openfeign进行生产级示例搭建,降低入门难度。

理论部分来自Seata官网:http://seata.io/zh-cn/docs/user/saga.html

二、整体机制

Saga模式是Seata提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第1张图片

理论基础:Hector & Kenneth 发表论⽂ Sagas (1987)

  • 适用场景:

    • 业务流程长、业务流程多
    • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
  • 优势:

    • 一阶段提交本地事务,无锁,高性能
    • 事件驱动架构,参与者可异步执行,高吞吐
    • 补偿服务易于实现
  • 缺点:

    • 不保证隔离性

三、Saga的实现

目前Seata 提供的Saga模式是基于状态机引擎来实现的,机制是:

  • 通过状态图来定义服务调用的流程并生成 json状态语言定义文件
  • 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
  • 状态图 json由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚

    注意:异常发生时是否进行补偿也可由用户自定义决定

  • 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

3.1 状态机设计器

Seata Saga 提供了一个可视化的状态机设计器方便用户使用:

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

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第2张图片

3.2 最佳实践

本示例未安装最佳实践进行,请自行创建事务控制表,参照:Seata TCC 模式下解决幂等、悬挂、空回滚问题 | Spring Cloud56 借鉴其思路,自行实践。

3.2.1 允许空补偿

  • 空补偿:原服务未执行,补偿服务执行了
  • 出现原因:
    • 原服务 超时(丢包)
    • Saga 事务触发 回滚
    • 未收到 原服务请求,先收到 补偿请求

所以服务设计时需要允许空补偿,即没有找到要补偿的业务主键时返回补偿成功并将原业务主键记录下来。

3.2.2 防悬挂控制

  • 悬挂:补偿服务 比 原服务 先执行
  • 出现原因:
    • 原服务 超时(拥堵)
    • Saga 事务回滚,触发 回滚
    • 拥堵的 原服务 到达

所以要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝服务的执行。

3.2.3 幂等控制

  • 原服务与补偿服务都需要保证幂等性,由于网络可能超时,可以设置重试策略,重试发生时要通过幂等控制避免业务数据重复更新。

3.2.4 缺乏隔离性的应对

  • 由于 Saga 事务不保证隔离性,在极端情况下可能由于脏写无法完成回滚操作, 比如举一个极端的例子,分布式事务内先给用户A充值, 然后给用户B扣减余额, 如果在给A用户充值成功, 在事务提交以前,A用户把余额消费掉了, 如果事务发生回滚, 这时则没有办法进行补偿了。这就是缺乏隔离性造成的典型的问题,实践中一般的应对方法是:
    • 业务流程设计时遵循“宁可长款, 不可短款”的原则,长款意思是客户少了钱机构多了钱,以机构信誉可以给客户退款,反之则是短款,少的钱可能追不回来了。所以在业务流程设计上一定是先扣款。
    • 有些业务场景可以允许让业务最终成功,在回滚不了的情况下可以继续重试完成后面的流程,所以状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力,让业务最终执行成功,达到最终一致性的目。

3.2.5 性能优化

  • 配置客户端参数client.rm.report.success.enable=false,可以在当分支事务执行成功时不上报分支状态到server,从而提升性能。

    当上一个分支事务的状态还没有上报的时候,下一个分支事务已注册,可以认为上一个实际已成功

四、示例说明

基于Seata Saga模式,演示分布式事务的提交和回滚。

本示例是一个商品下单的案例,一共有三个服务和一个公共模块:

  • order-saga:业务服务,用户下单操作将在这里完成。
  • account-saga:账户服务,可以查询/修改用户的账户信息
  • storage-saga:仓储服务,可以查询/修改商品的库存数量。
  • common-tcc:公共模块,包含:实体类、openfeign接口、统一异常处理等。

示例状态图如下:

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第3张图片

一个分布式事务内会有3Saga事务参与者,分别是:AccountServiceStorageServiceOrderService,其中:

  • AccountServiceStorageService为本地Bean(调用远程http服务),都有一个reduce方法,表示余额扣减或库存扣减或,还都有一个compensateReduce方法,表示补偿余额或库存扣减
  • OrderService为本地Bean,有一个createOrder方法,表示创建订单记录,还有一个compensateOrder方法,表示补偿(移除)订单记录

4.1 状态机构建

4.1.1 状态机完整json

对应的完整状态机json

{
  "nodes": [
    {
      "type": "node",
      "size": "80*72",
      "shape": "flow-rhombus",
      "color": "#13C2C2",
      "label": "AccountService-deduct-Choice",
      "stateId": "AccountService-deduct-Choice",
      "stateType": "Choice",
      "x": 467.875,
      "y": 286.5,
      "id": "c11238b3",
      "stateProps": {
        "Type": "Choice",
        "Choices": [
          {
            "Expression": "[deductResult] == true",
            "Next": "StorageService-deduct"
          }
        ],
        "Default": "Fail"
      },
      "index": 6
    },
    {
      "type": "node",
      "size": "39*39",
      "shape": "flow-circle",
      "color": "red",
      "label": "BService-save-catch",
      "stateId": "BService-save-catch",
      "stateType": "Catch",
      "x": 524.875,
      "y": 431.5,
      "id": "053ac3ac",
      "index": 7
    },
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "#FA8C16",
      "label": "Start",
      "stateId": "Start",
      "stateType": "Start",
      "stateProps": {
        "StateMachine": {
          "Name": "order",
          "Comment": "经典的分布式调用",
          "Version": "0.0.1"
        },
        "Next": "AService"
      },
      "x": 467.875,
      "y": 53,
      "id": "973bd79e",
      "index": 11
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-rect",
      "color": "#1890FF",
      "label": "AccountService-deduct",
      "stateId": "AccountService-deduct",
      "stateType": "ServiceTask",
      "stateProps": {
        "Type": "ServiceTask",
        "ServiceName": "accountService",
        "Next": "AccountService-deduct-Choice",
        "ServiceMethod": "deduct",
        "Input": [
          "$.[businessKey]",
          "$.[userId]",
          "$.[commodityCode]",
          "$.[count]"
        ],
        "Output": {
          "deductResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "CompensateState": "AccountService-compensateDeduct",
        "Retry": []
      },
      "x": 467.875,
      "y": 172,
      "id": "e17372e4",
      "index": 12
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-rect",
      "color": "#1890FF",
      "label": "StorageService-deduct",
      "stateId": "StorageService-deduct",
      "stateType": "ServiceTask",
      "stateProps": {
        "Type": "ServiceTask",
        "ServiceName": "storageService",
        "ServiceMethod": "deduct",
        "CompensateState": "StorageService- compensateDeduct",
        "Input": [
          "$.[businessKey]",
          "$.[userId]",
          "$.[commodityCode]",
          "$.[count]"
        ],
        "Output": {
          "deductResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Next": "StorageService-deduct-Choice"
      },
      "x": 467.125,
      "y": 411,
      "id": "a6c40952",
      "index": 13
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-capsule",
      "color": "#722ED1",
      "label": "AccountService-compensateDeduct",
      "stateId": "AccountService-compensateDeduct",
      "stateType": "Compensation",
      "stateProps": {
        "Type": "Compensation",
        "ServiceName": "accountService",
        "ServiceMethod": "compensateDeduct",
        "Input": [
          "$.[businessKey]",
          "$.[userId]",
          "$.[commodityCode]",
          "$.[count]"
        ]
      },
      "x": 260.625,
      "y": 172.5,
      "id": "3b348652",
      "index": 14
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-capsule",
      "color": "#722ED1",
      "label": "StorageService-compensateDeduct",
      "stateId": "StorageService-compensateDeduct",
      "stateType": "Compensation",
      "stateProps": {
        "Type": "Compensation",
        "ServiceName": "storageService",
        "ServiceMethod": "compensateDeduct",
        "Input": [
          "$.[businessKey]",
          "$.[userId]",
          "$.[commodityCode]",
          "$.[count]"
        ]
      },
      "x": 262.125,
      "y": 411,
      "id": "13b600b1",
      "index": 15
    },
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "#05A465",
      "label": "Succeed",
      "stateId": "Succeed",
      "stateType": "Succeed",
      "x": 466.625,
      "y": 795,
      "id": "690e5c5e",
      "stateProps": {
        "Type": "Succeed"
      },
      "index": 16
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-capsule",
      "color": "red",
      "label": "Compensation\nTrigger",
      "stateId": "CompensationTrigger",
      "stateType": "CompensationTrigger",
      "x": 881.625,
      "y": 430.5,
      "id": "757e057f",
      "stateProps": {
        "Type": "CompensationTrigger",
        "Next": "Fail"
      },
      "index": 17
    },
    {
      "type": "node",
      "size": "72*72",
      "shape": "flow-circle",
      "color": "red",
      "label": "Fail",
      "stateId": "Fail",
      "stateType": "Fail",
      "stateProps": {
        "Type": "Fail",
        "ErrorCode": "FAILED",
        "Message": "buy failed"
      },
      "x": 881.125,
      "y": 285.5,
      "id": "0131fc0c",
      "index": 18
    },
    {
      "type": "node",
      "size": "39*39",
      "shape": "flow-circle",
      "color": "red",
      "label": "AccountService-deduct-catch",
      "stateId": "AccountService-deduct-catch",
      "stateType": "Catch",
      "x": 518.125,
      "y": 183,
      "id": "0955401d"
    },
    {
      "type": "node",
      "size": "80*72",
      "shape": "flow-rhombus",
      "color": "#13C2C2",
      "label": "StorageService-deduct-Choice",
      "stateId": "StorageService-deduct-Choice",
      "stateType": "Choice",
      "x": 466.875,
      "y": 545.5,
      "id": "27978f5d",
      "stateProps": {
        "Type": "Choice",
        "Choices": [
          {
            "Expression": "[deductResult] == true",
            "Next": "OrderService-createOrder"
          }
        ],
        "Default": "Fail"
      }
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-rect",
      "color": "#1890FF",
      "label": "OrderService-createOrder",
      "stateId": "OrderService-createOrder",
      "stateType": "ServiceTask",
      "stateProps": {
        "Type": "ServiceTask",
        "ServiceName": "orderService",
        "ServiceMethod": "createOrder",
        "CompensateState": "OrderService- compensateOrder",
        "Input": [
          "$.[businessKey]",
          "$.[userId]",
          "$.[commodityCode]",
          "$.[count]"
        ],
        "Output": {
          "createOrderResult": "$.#root"
        },
        "Status": {
          "#root == true": "SU",
          "#root == false": "FA",
          "$Exception{java.lang.Throwable}": "UN"
        },
        "Next": "Succeed"
      },
      "x": 466.625,
      "y": 676,
      "id": "9351460d"
    },
    {
      "type": "node",
      "size": "110*48",
      "shape": "flow-capsule",
      "color": "#722ED1",
      "label": "OrderService-compensateOrder",
      "stateId": "OrderService-compensateOrder",
      "stateType": "Compensation",
      "stateProps": {
        "Type": "Compensation",
        "ServiceName": "orderService",
        "ServiceMethod": "compensateOrder",
        "Input": [
          "$.[businessKey]",
          "$.[userId]",
          "$.[commodityCode]",
          "$.[count]"
        ]
      },
      "x": 261.625,
      "y": 675.5,
      "id": "b2789952"
    },
    {
      "type": "node",
      "size": "39*39",
      "shape": "flow-circle",
      "color": "red",
      "label": "OrderService-createOrder-catch",
      "stateId": "OrderService-createOrder-catch",
      "stateType": "Catch",
      "x": 523.125,
      "y": 696,
      "id": "466cf242"
    }
  ],
  "edges": [
    {
      "source": "973bd79e",
      "sourceAnchor": 2,
      "target": "e17372e4",
      "targetAnchor": 0,
      "id": "f0a9008f",
      "index": 0
    },
    {
      "source": "e17372e4",
      "sourceAnchor": 2,
      "target": "c11238b3",
      "targetAnchor": 0,
      "id": "cd8c3104",
      "index": 2,
      "label": "执行结果",
      "shape": "flow-smooth"
    },
    {
      "source": "c11238b3",
      "sourceAnchor": 2,
      "target": "a6c40952",
      "targetAnchor": 0,
      "id": "e47e49bc",
      "stateProps": {},
      "label": "执行成功",
      "shape": "flow-smooth",
      "index": 3
    },
    {
      "source": "c11238b3",
      "sourceAnchor": 1,
      "target": "0131fc0c",
      "targetAnchor": 3,
      "id": "e3f9e775",
      "stateProps": {},
      "label": "执行失败",
      "shape": "flow-smooth",
      "index": 4
    },
    {
      "source": "053ac3ac",
      "sourceAnchor": 1,
      "target": "757e057f",
      "targetAnchor": 3,
      "id": "3f7fe6ad",
      "stateProps": {
        "Exceptions": [
          "java.lang.Throwable"
        ],
        "Next": "CompensationTrigger"
      },
      "label": "StorageService-deduct异常触发补偿",
      "shape": "flow-smooth",
      "index": 5
    },
    {
      "source": "e17372e4",
      "sourceAnchor": 3,
      "target": "3b348652",
      "targetAnchor": 1,
      "id": "52a2256e",
      "style": {
        "lineDash": "4"
      },
      "index": 8,
      "label": "",
      "shape": "flow-smooth"
    },
    {
      "source": "a6c40952",
      "sourceAnchor": 3,
      "target": "13b600b1",
      "targetAnchor": 1,
      "id": "474512d9",
      "style": {
        "lineDash": "4"
      },
      "index": 9
    },
    {
      "source": "757e057f",
      "sourceAnchor": 0,
      "target": "0131fc0c",
      "targetAnchor": 2,
      "id": "1abf48fa",
      "index": 10
    },
    {
      "source": "0955401d",
      "sourceAnchor": 1,
      "target": "757e057f",
      "targetAnchor": 1,
      "id": "654280aa",
      "shape": "flow-polyline-round",
      "stateProps": {
        "Exceptions": [
          "java.lang.Throwable"
        ],
        "Next": "CompensationTrigger"
      },
      "label": "AccountService-deduct异常触发补偿"
    },
    {
      "source": "a6c40952",
      "sourceAnchor": 2,
      "target": "27978f5d",
      "targetAnchor": 0,
      "id": "f25a12eb",
      "shape": "flow-polyline-round",
      "label": "执行结果"
    },
    {
      "source": "27978f5d",
      "sourceAnchor": 2,
      "target": "9351460d",
      "targetAnchor": 0,
      "id": "99d78285",
      "shape": "flow-smooth",
      "stateProps": {},
      "label": "执行成功"
    },
    {
      "source": "9351460d",
      "sourceAnchor": 2,
      "target": "690e5c5e",
      "targetAnchor": 0,
      "id": "82670a92",
      "shape": "flow-polyline-round"
    },
    {
      "source": "9351460d",
      "sourceAnchor": 3,
      "target": "b2789952",
      "targetAnchor": 1,
      "id": "5db6a545",
      "shape": "flow-polyline-round",
      "style": {
        "lineDash": "4",
        "endArrow": false
      },
      "type": "Compensation"
    },
    {
      "source": "466cf242",
      "sourceAnchor": 1,
      "target": "757e057f",
      "targetAnchor": 2,
      "id": "a9f55df2",
      "shape": "flow-polyline-round",
      "stateProps": {
        "Exceptions": [
          "java.lang.Throwable"
        ],
        "Next": "CompensationTrigger"
      },
      "label": "OrderService-createOrder异常触发补偿"
    },
    {
      "source": "27978f5d",
      "sourceAnchor": 1,
      "target": "0131fc0c",
      "targetAnchor": 1,
      "id": "c303cae6",
      "shape": "flow-polyline-round",
      "stateProps": {},
      "label": "执行失败"
    }
  ]
}

4.1.2 开始节点Start

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第4张图片
说明:

  • StateMachine下配置的是"状态机" 属性
    • Name:表示状态机的名称,必须唯一
    • Comment:状态机的描述
    • Version:状态机定义版本
  • Next:服务执行完成后下一个执行的"状态"(下一个执行的"状态"节点的ID

4.1.3 任务节点ServiceTask

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第5张图片
完整Props内容:

{
  "Type": "ServiceTask",
  "ServiceName": "accountService",
  "Next": "AccountService-deduct-Choice",
  "ServiceMethod": "deduct",
  "Input": [
    "$.[businessKey]",
    "$.[userId]",
    "$.[commodityCode]",
    "$.[count]"
  ],
  "Output": {
    "deductResult": "$.#root"
  },
  "Status": {
    "#root == true": "SU",
    "#root == false": "FA",
    "$Exception{java.lang.Throwable}": "UN"
  },
  "CompensateState": "AccountService-compensateDeduct",
  "Retry": []
}

说明:

  • ServiceName:服务名称,通常是服务的beanId

  • ServiceMethod:服务方法名称

  • CompensateState:该"状态"的补偿"状态"(补偿"状态"节点的ID

  • Input:调用服务的输入参数列表, 是一个数组,对应于服务方法的参数列表,$.表示使用表达式从状态机上下文中取参数,表达使用的SpringEL,如果是常量直接写值即可。

  • Output:将服务返回的参数赋值到状态机上下文中, 是一个map结构,key为放入到状态机上文时的key(状态机上下文也是一个map),value$.是表示SpringEL表达式,表示从服务的返回参数中取值,#root表示服务的整个返回参数

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

  • Retry: 捕获异常后的重试策略,是个数组可以配置多个规则,Exceptions 为匹配的的异常列表, IntervalSeconds 为重试间隔, MaxAttempts 为最大重试次数,BackoffRate 下一次重试间隔相对于上一次重试间隔的倍数,比如说上次一重试间隔是2秒,BackoffRate=1.5 则下一次重试间隔是3秒。Exceptions 属性可以不配置,不配置时表示框架自动匹配网络超时异常。当在重试过程中发生了别的异常,框架会重新匹配规则,并按新规则进行重试,同一种规则的总重试次数不会超过该规则的MaxAttempts

    "Retry": [
        {
            "Exceptions": ["io.seata.saga.engine.mock.DemoException"],
            "IntervalSeconds": 1.5,
            "MaxAttempts": 3,
            "BackoffRate": 1.5
        },
        {
            "IntervalSeconds": 1,
            "MaxAttempts": 3,
            "BackoffRate": 1.5
        }
    ]
    
  • Next:服务执行完成后下一个执行的"状态"(下一个执行的"状态"节点的ID

  • IsForUpdate:标识该服务会更新数据, 默认是false,如果配置了CompensateState则默认是true,有补偿服务的服务肯定是数据更新类服务

  • IsPersist:执行日志是否进行存储,默认是true,有一些查询类的服务可以配置为false,执行日志不进行存储提高性能,因为当异常恢复时可以重复执行

  • IsAsync:异步调用服务,注意:因为异步调用服务会忽略服务的返回结果,所以用户定义的服务执行状态映射(下面的Status属性)将被忽略,默认为服务调用成功, 如果提交异步调用就失败(比如线程池已满)则为服务执行状态为失败

  • IsRetryPersistModeUpdate:向前重试时,日志是否基于上次失败日志进行更新,默认是false,即新增一条重试日志 (优先级高于状态机属性配置)

  • IsCompensatePersistModeUpdate:向后补偿重试时,日志是否基于上次补偿日志进行更新,默认是false, 即新增一条补偿日志 (优先级高于状态机属性配置)

  • Loop:标识该事务节点是否为循环事务,即由框架本身根据循环属性的配置,遍历集合元素对该事务节点进行循环执行。具体使用见:http://seata.io/zh-cn/docs/user/saga.html

当没有配置Status对服务执行状态进行映射, 系统会自动判断状态:

  • 没有异常则认为执行成功
  • 如果有异常,则判断异常是不是网路连接超时, 如果是则认为是FA
  • 如果是其它异常,服务IsForUpdate=true则状态为UN, 否则为FA

整个状态机的执行状态如何判断?
是由框架自己判断的, 状态机有两个状态: status(正向执行状态), compensateStatus(补偿状态)

  • 如果所有服务执行成功(事务提交成功)则status=SU, compensateStatus=null
  • 如果有服务执行失败且存在更新类服务执行成功且没有进行补偿(事务提交失败) 则status=UN, compensateStatus=null
  • 如果有服务执行失败且不存在更新类服务执行成功且没有进行补偿(事务提交失败) 则status=FA, compensateStatus=null
  • 如果补偿成功(事务回滚成功)则status=FA/UN, compensateStatus=SU
  • 发生补偿且有未补偿成功的服务(回滚失败)则status=FA/UN, compensateStatus=UN
  • 存在事务提交或回滚失败的情况Seata Sever都会不断发起重试

4.1.4 选择节点Choice

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第6张图片
说明:
Choice类型的"状态"是单项选择路由 Choices

  • 可选的分支列表,只会选择第一个条件成立的分支 ExpressionSpringEL表达式
  • Next:服务执行完成后下一个执行的"状态"(下一个执行的"状态"节点的ID

重要:下面是choice线条默认的属性,如果不修改配置会出现异常,正确为Props:{}

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第7张图片

4.1.5 成功节点Succeed

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第8张图片

说明:

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

4.1.6 失败节点Fail

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第9张图片

说明:

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

4.1.7 补偿触发节点CompensationTrigger

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第10张图片

说明:

  • CompensationTrigger类型的state是用于触发补偿事件,回滚分布式事务
  • Next:服务执行完成后下一个执行的"状态"(下一个执行的"状态"节点的ID

4.1.8 补偿节点Compensation

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第11张图片

说明:

  • ServiceName:服务名称,通常是服务的beanId
  • ServiceMethod:服务方法名称
  • Input:调用服务的输入参数列表, 是一个数组,对应于服务方法的参数列表,$.表示使用表达式从状态机上下文中取参数,表达使用的SpringEL,如果是常量直接写值即可。

4.1.9 异常捕获节点

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第12张图片
Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57_第13张图片

说明:

  • 注释 1 异常捕捉节点要通过图形覆盖在状态任务节点的图形上,实现2者之间的关联
  • 注释 2 通过对Catch节点发散出去的箭头线上的属性配置,指定对什么异常进行捕获,以及捕获到对应的异常后,指定下一个执行的"状态"(下一个执行的"状态"节点的ID

4.1.10 如何触发补偿

为了提高灵活性。用户可以自己控制是否进行回滚,因为并不是所有异常都要回滚,可能有一些自定义处理手段:

  • 补偿节点不能够自动触发补偿,对需要补偿的必须手动在状态机json中,由 Catch 或者 Choices 属性路由到 CompensationTrigger,或是参照章节4.1.9的实现方式

4.1.11 复杂参数的Input定义

复杂参数示例json

{
    "Type": "ServiceTask",
    "ServiceName": "demoService",
    "ServiceMethod": "complexParameterMethod",
    "Next": "ChoiceState",
    "ParameterTypes" : ["java.lang.String", "int", "io.seata.saga.engine.mock.DemoService$People", "[Lio.seata.saga.engine.mock.DemoService$People;", "java.util.List", "java.util.Map"],
    "Input": [
        "$.[people].name",
        "$.[people].age",
        {
            "name": "$.[people].name",
            "age": "$.[people].age",
            "childrenArray": [
                {
                    "name": "$.[people].name",
                    "age": "$.[people].age"
                },
                {
                    "name": "$.[people].name",
                    "age": "$.[people].age"
                }
            ],
            "childrenList": [
                {
                    "name": "$.[people].name",
                    "age": "$.[people].age"
                },
                {
                    "name": "$.[people].name",
                    "age": "$.[people].age"
                }
            ],
            "childrenMap": {
                "lilei": {
                    "name": "$.[people].name",
                    "age": "$.[people].age"
                }
            }
        },
        [
            {
                "name": "$.[people].name",
                "age": "$.[people].age"
            },
            {
                "name": "$.[people].name",
                "age": "$.[people].age"
            }
        ],
        [
            {
                "@type": "io.seata.saga.engine.mock.DemoService$People",
                "name": "$.[people].name",
                "age": "$.[people].age"
            }
        ],
        {
            "lilei": {
                "@type": "io.seata.saga.engine.mock.DemoService$People",
                "name": "$.[people].name",
                "age": "$.[people].age"
            }
        }
    ],
    "Output": {
        "complexParameterMethodResult": "$.#root"
    }
}

上面的complexParameterMethod方法定义如下:

People complexParameterMethod(String name, int age, People people, People[] peopleArrya, List<People> peopleList, Map<String, People> peopleMap)

@Data
class People {
    private String name;
    private int age;
    private People[] childrenArray;
    private List<People> childrenList;
    private Map<String, People> childrenMap;
}

启动状态机时传入参数:

Map<String, Object> paramMap = new HashMap<>(1);
People people = new People();
people.setName("lilei");
people.setAge(18);
paramMap.put("people", people);
// 状态机名称
String stateMachineName = "demo";
StateMachineInstance inst = stateMachineEngine.start(stateMachineName, null, paramMap);

注意:ParameterTypes属性是可以不用传的,调用的方法的参数列表中有MapList这种可以带泛型的集合类型,因为java编译会丢失泛型,所以需要用这个属性,同时在Inputjson中对应的对这个json@type来申明泛型(集合的元素类型)

4.1.12 Saga的json文件热部署

通过:stateMachineEngine.getStateMachineConfig().getStateMachineRepository().registryByResources()。不过java代码和服务需要自己实现支持热部署。

4.1.13 未完成的状态机实例恢复

Seata Saga 开启事务的客户端或者Seata Server服务端宕机或者重启,未完成的状态机实例,通过读取本地数据库有记录日志,通过日志恢复。Seata Server会触触发事务恢复。

4.2 Saga状态机所需表结构

脚本所在GitHub地址:https://github.com/seata/seata/tree/1.6.1/script/client/saga/db

建表语句如下(MySQL 语法):

CREATE TABLE IF NOT EXISTS `seata_state_machine_def`
(
    `id`               VARCHAR(32)  NOT NULL COMMENT 'id',
    `name`             VARCHAR(128) NOT NULL COMMENT 'name',
    `tenant_id`        VARCHAR(32)  NOT NULL COMMENT 'tenant id',
    `app_name`         VARCHAR(32)  NOT NULL COMMENT 'application name',
    `type`             VARCHAR(20)  COMMENT 'state language type',
    `comment_`         VARCHAR(255) COMMENT 'comment',
    `ver`              VARCHAR(16)  NOT NULL COMMENT 'version',
    `gmt_create`       DATETIME(3)  NOT NULL COMMENT 'create time',
    `status`           VARCHAR(2)   NOT NULL COMMENT 'status(AC:active|IN:inactive)',
    `content`          TEXT COMMENT 'content',
    `recover_strategy` VARCHAR(16) COMMENT 'transaction recover strategy(compensate|retry)',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `seata_state_machine_inst`
(
    `id`                  VARCHAR(128)            NOT NULL COMMENT 'id',
    `machine_id`          VARCHAR(32)             NOT NULL COMMENT 'state machine definition id',
    `tenant_id`           VARCHAR(32)             NOT NULL COMMENT 'tenant id',
    `parent_id`           VARCHAR(128) COMMENT 'parent id',
    `gmt_started`         DATETIME(3)             NOT NULL COMMENT 'start time',
    `business_key`        VARCHAR(48) COMMENT 'business key',
    `start_params`        TEXT COMMENT 'start parameters',
    `gmt_end`             DATETIME(3) COMMENT 'end time',
    `excep`               BLOB COMMENT 'exception',
    `end_params`          TEXT COMMENT 'end parameters',
    `status`              VARCHAR(2) COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `compensation_status` VARCHAR(2) COMMENT 'compensation status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `is_running`          TINYINT(1) COMMENT 'is running(0 no|1 yes)',
    `gmt_updated`         DATETIME(3) NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `unikey_buz_tenant` (`business_key`, `tenant_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `seata_state_inst`
(
    `id`                       VARCHAR(48)  NOT NULL COMMENT 'id',
    `machine_inst_id`          VARCHAR(128) NOT NULL COMMENT 'state machine instance id',
    `name`                     VARCHAR(128) NOT NULL COMMENT 'state name',
    `type`                     VARCHAR(20)  COMMENT 'state type',
    `service_name`             VARCHAR(128) COMMENT 'service name',
    `service_method`           VARCHAR(128) COMMENT 'method name',
    `service_type`             VARCHAR(16) COMMENT 'service type',
    `business_key`             VARCHAR(48) COMMENT 'business key',
    `state_id_compensated_for` VARCHAR(50) COMMENT 'state compensated for',
    `state_id_retried_for`     VARCHAR(50) COMMENT 'state retried for',
    `gmt_started`              DATETIME(3)  NOT NULL COMMENT 'start time',
    `is_for_update`            TINYINT(1) COMMENT 'is service for update',
    `input_params`             TEXT COMMENT 'input parameters',
    `output_params`            TEXT COMMENT 'output parameters',
    `status`                   VARCHAR(2)   NOT NULL COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    `excep`                    BLOB COMMENT 'exception',
    `gmt_updated`              DATETIME(3) COMMENT 'update time',
    `gmt_end`                  DATETIME(3) COMMENT 'end time',
    PRIMARY KEY (`id`, `machine_inst_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

因篇幅原因本章就先到这里,具体示例代码搭建请见下回分解。

你可能感兴趣的:(spring,cloud,seata,saga)