作者:王特(亦夏)
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 为用户提供了 AT、TCC、SAGA、XA 等多种事务模式,帮助解决不同业务场景下的事务一致性问题。
本文主要介绍 Seata Saga 模式的使用以及最佳实践,围绕三个部分展开,第一部分是 Seata Saga 的简介、第二部分是带大家快速入门,学习怎么使用 Seata Saga 模式,最后一部分将会给大家分享一些 Seata Saga 实践中的经验,帮助用户更快、更好得使用 Seata Saga 模式。
Saga 模式是分布式事务的解决方案之一,理念起源于 1987 年 Hector & Kenneth 发表的 Sagas 论文。它将整个分布式事务流程拆分成多个阶段,每个阶段对应我们的子事务,子事务是本地事务执行的,执行完成就会真实提交。
它是一种基于失败的设计,如上图,可以看到,每个活动或者子事务流程,一般都会有对应的补偿服务。如果分布式事务发生异常的话,在 SAGA 模式中,就要进行所谓的‘恢复’ ,恢复有两种方式,逆向补偿和正向重试。比如上面的分布式事务执行到 T3 失败,逆向补偿将会依次执行对应的 C3,C2,C1 操作,取消事务活动的 ‘影响’。那正向补偿,它是一往无前,T3 失败了,会进行不断重试,然后继续按照流程执行 T4,T5 等。
根据 Saga 模式的设计,我们可以得到 Saga 事务模式的优缺点。
优点:
缺点:
所以 Saga 模式的使用也需要考虑这些问题带来的‘影响’。一般 Saga 模式的使用场景有如下几个:
接下来我们看看 Seata Saga 的实现,Saga 主流的实现分为两种:编排式和协调式。 Seata Saga 的实现方式是编排式,是基于状态机引擎实现的。状态机执行的最小单位是节点:节点可以表示一个服务调用,对应 Saga 事务就是子事务活动/流程,也可以配置其补偿节点,通过链路的串联,编排出一个状态机调用流程。在 Seata 里,调用流程目前使用 JSON 描述,由状态机引擎驱动执行,当异常的时候,我们也可以选择补偿策略,由 Seata 协调者端触发事务补偿。
有没有感觉像是服务编排,区别于服务编排,Seata Saga 状态机是 Saga+ 服务编排,支持补偿服务,保证最终一致性。
我们来看看一个简单的状态机流程定义:
上方是一个 Name 为 reduceIncentoryAndBalance 的状态机描述,里面定了 ServiceTask 类型的服务调用节点以及对应的补偿节点 CompensateReduceInventory。
看看几个基本的属性:
更多类型和语法可以参考 Seata 官方文档 [ 1] ,可以看到状态机 JSON 声明还是有些难度的,为了简化状态机 JSON 的编写,我们也提供了可视化的编排界面 [ 2] ,如下所示,编排了一个较为复杂的流程。
话不多说,我们进入下面的实践环节。
Seata 分 TC、TM 和 RM 三个角色,TC(Server 端)为单独服务端部署,TM 和 RM(Client 端)由业务系统集成。
Server 端存储模式(store.mode)现有 file、db、redis 三种(后续将引入 raft,mongodb),file 模式无需改动,直接启动即可。
从新人文档,可以看出 Seata 还是传统的 CS 模型。首先我们需要部署下 Seata Server 端。Server 端默认的存储模式是 file 模式,无需改动,直接执行 springboot 启动类 main 方法即可启动 Seata Server。为了方便,本次演示就使用 file 模式启动,其他模式的启动方式可以参考新人文档的详细介绍。
同时我们需要创建一个客户端的测试应用,这里命名 seata-saga-test,测试应用使用 springboot 框架,配置好 spring 的 aplication.pname 和 port,并且引入 seata-spring-boot-starter 依赖,完成 Client 端应用的搭建。
一般了解一个框架的功能,建议是从入口的单元测试类开始看起。在 Seata 仓库中找到 Seata Saga 的 test 模块,从最外围的测试类 io.seata.saga.engine.StateMachineTests 看起(一般开源项目最外围的测试类即是入口类):
从上面的截图可以看出,入口测试方法主要分为三个部分。
【1】处的 spring 配置文件声明了 StateMachineEngine Bean 以及对应的属性,【2】处也引用了该类执行 start,判断该类为我们状态机的入口类,其实 StateMachineEngine 该类也就是 Seata Saga 状态机操作入口,控制状态机的开始、恢复等操作。StateMachineEngine 有一个重要的属性 resources,该属性声明了状态机 JSON 文件的存储路径,Seata Saga 状态机引擎启动的时候会加载对应路径下的状态机定义,以供后续使用,这里的路径根据我们需求更改。
【3】处调用了 StateMachineEngine 的 start 方法,传递状态机名称,启动参数,开启一个状态机流程调用,简单跟下实现,可以看到其中状态名称对应 resources 路径下状态机 JSON 定义中的 Name 属性。
测试 Seata Saga 状态机流程,我们得先有一个状态机 JSON 定义。使用 Seata Saga StateMachine Designer,定义一个简单 AService#doA 方法调用 BService#doB 方法的状态机流程,再加个入参,最终我们的类#方法和状态机 JSON 如下所示。
有了基础的调用模型和状态机 JSON 定义,按照测试用例,我们同样声明出状态机 Bean 及执行入口 (注意:start 方法里面的状态机名称需要和状态机 JSON 定义里面的 Name 名称保持一致) ,执行下 main 方法,我们可以发现 AService#doA 方法和 BService#doB 方法都被成功调用了。
至此,我们已经完成了 Seata Saga 状态机的入门使用。继续观察单测,我们发现 Seata Saga 单测还有两个模块,分表是 db 和 mock。
我们先来看看 db 模块的单测,可以看到 db 模块的单测类和上面基本类似,唯一的区别就在于 StateMachineEngine,指定了 db 存储,执行了 ddl sql(初始化 Seata Saga 相关表)。指定了 db 存储,那么我们的状态机执行过程将会持久化在 db 存储,方便事务执行过程查询和异常恢复,也是生产环境的实践方式。
mock 模块通过 mock transcation,脱离 Seata Sever,仅使用了 Seata Saga 的服务编排能力。有兴趣的同学可以再去实践下 db 和 mock 模块的使用,这里就不展开了。
基于 DB 存储的 Saga 模式,需要注意:重试或者补偿默认会插入一条状态执行记录,频繁重试或者补偿,会导致状态执行记录爆炸,如果有大对象存储,可能会导致内存 crash。Seata Saga 提供了 update 模式,使用 update 记录代替新增执行记录,用来避免此类问题。
讲了这么多,Seata Saga 目前状态机的实现,上手成本相对还是比较高。一方面我们致力提升 Seata Saga 状态机模式的易用性,同时也在设计 Saga 的注解化模式、流式编排模式,期望提供给用户更具产品化能力的 Seata Saga。有兴趣的同学,也非常欢迎加入共建,搜索钉钉群号,加入Seata Group 开源交流群。(群号:44816898)
相关链接:
[1] Seata 官方文档
http://seata.io/zh-cn/docs/user/saga.html
[2] 可视化的编排界面
http://seata.io/saga_designer/index.html#/