SpringCloudAlibaba分布式事务解决方案Seata实战与源码分析-中

  事务模式

  概述

  在当前的技术发展阶段,不存一个分布式事务处理机制可以完美满足所有场景的需求。一致性、可靠性、易用性、性能等诸多方面的系统设计约束,需要用不同的事务处理机制去满足。 目前使用的流行度情况是:AT>TCC > Saga,Seata 项目最核心的价值在于:构建一个全面解决分布式事务问题的标准化平台。基于 Seata,上层应用架构可以根据实际场景的需求,灵活选择合适的分布式事务解决方案。 Seata针对不同的业务场景提供了四种不同的事务模式,对比如下: AT模式:AT 模式的一阶段、二阶段提交和回滚(借助undo_log表来实现)均由 Seata 框架自动生成,用户只需编写“业务SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。 TCC模式:相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT模式高很多。适用于核心系统等对性能有很高要求的场景。 SAGA模式:Sage 是长事务解决方案,事务驱动,使用那种存在流程审核的业务场景。 XA模式:XA模式是分布式强一致性的解决方案,但性能低而使用较少。

  AT模式

  基础理论

  使用AT模式的前提条件 基于支持本地 ACID 事务的关系型数据库。 Java 应用,通过 JDBC 访问数据库。

  AT模式支持的数据库有:MySQL、Oracle、PostgreSQL、 TiDB、MariaDB。

  整体机制为两阶段提交协议的演变 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。 二阶段: 提交异步化,非常快速地完成。 回滚通过一阶段的回滚日志进行反向补偿。

  写隔离 一阶段本地事务提交前,需要确保先拿到 全局锁 。 拿不到 全局锁 ,不能提交本地事务。 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

  读隔离 在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。 如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

  工作机制 假如分支事务逻辑为:update product set name = 'GTS' where name = 'TXC'; 业务表product 表字段如下

  image-20220712103504661

  一阶段

  解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。select id, name, since from product where name = 'TXC';

  查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。前镜像为image-20220712103808963

  执行业务 SQL:更新这条记录的 name 为 'GTS'。

  查询后镜像:根据前镜像的结果,通过 主键 定位数据。select id, name, since from product where id = 1;后镜像为image-20220712103903331

  插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 表中。

  提交前,向 TC 注册分支:申请 表中,主键值等于 1 的记录的 全局锁 。

  本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。

  将本地事务提交的结果上报给 TC。

  二阶段 回滚 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的可去查阅官网文档。 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:update product set name = 'TXC' where id = 1; 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。 提交 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

  实战示例

  我们重新修改下上一小节seata,专门定义一个group: SEATA_GROUP,然后重新启动seata

  我们创建订单和库存两个微服务来测试AT事务模式 订单服务:根据采购需求创建订单。 库存服务:对给定的商品扣除仓储数量。

  SEATA AT 模式需要 表, 表语句在源码目录下scriptclientatdbmysql.sql里

  创建订单数据库storage,创建库存业务表storage_tbl和undo_log

  创建库存数据库order,创建订单业务表order_tbl

  image-20220713150525635

  引入seata依赖,由于库存微服务和订单微服务都引用ecom-commons,所以在ecom-commons的pom中添加如下seata依赖,注意这里web容器建议使用tomcat,原来我使用的是undertow,但是执行分布式事务报错所以最后注释使用undertow

  image-20220717005155846

  由于使用最新版本seata-all依赖需要class字节码版本为55也即是JDK11,因此演示项目使用的JDK11,库存微服务和订单微服务微服务的启动配置文件中加入如下配置

  订单控制器提供订单创建接口

  image-20220713171908476

  库存控制器提供扣减库存接口

  image-20220713172002569

  通过在订单的实现类上添加一个@GlobalTransactional注解实现分布式事务

  image-20220713172312967

  目前是一个正常场景,访问http://localhost:4070/order/create/1000/1001/2 返回成功

  image-20220713172357497

  库存成功扣减2个

  image-20220713172437200

  订单表也增加一条记录

  image-20220713172505444

  库存实现类增加异常代码

  image-20220713173714822

  访问http://localhost:4070/order/create/1000/1001/2 返回失败

  image-20220713173128755

  库存表没有减库存,订单表也没有新的订单记录

  image-20220713173334737

  在订单的实现类实现类创建订单方法中去掉@GlobalTransactional后再次访问,这时候就没有分布式事务支持,再次访问后订单表有添加新的订单记录,但是库存表没有减少,至此AT事务模式的实例完整结束。

  调试看过程

  在订单实现类create方法和库存实现类的deduct方法入口加上端口,重新启动两个微服务

  image-20220717010555501

  查看seata server的运行日志,出现库存和订单微服务已经注册到seata中

  image-20220717010501868

  查看nacos服务列表,由于我启动两个订单微服务

  image-20220717010853932

  访问http://localhost:4070/order/create/1000/1001/2 ,进入第一个断点,注意整个分布式事务不要超过超时时间默认为60秒,可以配置,我是快速截图保存

  image-20220717011156158

  当断点走完库存返回查看几张表如下,锁表记录

  image-20220717011745582

  分支表记录

  image-20220717012407573

  库存表undo_log表信息如下

  image-20220717012427417

  rollback_info信息,核心是beforeImage和afterImage

  订单表的undo_log表信息如下

  image-20220717012835064

  订单表的undolog的rollback_info信息,核心是beforeImage和afterImage

  XA模式

  前置理论

  刚性事务指的是分布式事务要像本地式事务⼀样,具备数据强⼀致性。从 CAP 来看就是要达到 CP 状态。常见的刚性事务方案有:XA 协议(2PC、JTA、JTS)、3PC。由于刚性事务同步阻塞,处理效率低,不适合⼤型⽹站分布式场景。

  XA 规范是 X/Open 组织定义的分布式事务处理 (DTP,Distributed Transaction Processing) 标准。规范描述了全局的事务管理器与局部的资源管理器之间的接口。对于 XA 模型,包含三个角色: AP:Applicaiton,应用程序业务层,哪些操作属于⼀个事务,就是 AP 定义的。 TM:Transaction Manager:接收 AP 的事务请求,对全局事务进⾏管理,管理事务分⽀状态,协调 RM 的处理,通知 RM 哪些操作属于哪些全局事务以及事务分⽀等等。这个也是整个事务调度模型的核⼼部分。 RM:Resource Manager,资源管理器:⼀般是数据库,也可以是其他的资源管理器,如消息队列 (如 JMS 数据源),⽂件系统等。

  XA 规范的目的是允许多个资源 (如数据库,应用服务器,消息队列等) 在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。XA 规范使用两阶段提交 (2PC,Two-Phase Commit) 协议来保证所有资源同时提交或回滚任何特定的事务。目前知名的数据库,如 Oracle, DB2, mysql 等,都是实现了 XA 接口的,都可以作为 RM。

  XA 规范定义了 (全局) 事务管理器 (Transaction Manager) 和 (局部) 资源管理器 (Resource Manager) 之间的接口。XA 接口是双向的系统接口,在事务管理器 (Transaction Manager) 以及一个或多个资源管理器 (Resource Manager) 之间形成通信桥梁。

  XA 之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲,两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源 (如数据库或 JMS 队列)

  XA 是数据库的分布式事务,强一致性,在整个过程中,数据一张锁住状态,即从 prepare 到 commit、rollback 的整个过程中,TM 一直把持折数据库的锁,如果有其他人要修改数据库的该条数据,就必须等待锁的释放,存在⻓事务⻛险。

  为什么要在Seata中支持XA

  Seata 已经支持了三大事务模式:AT、TCC、SAGA,这三个都是补偿型事务,补偿型事务处理你机制构建在 事务资源之上(要么中间件层面,要么应用层),事务资源本身对于分布式的事务是无感知的,这种对于分布式事务的无感知存在有一个根本性的问题,无法做到真正的全局一致性。

  例如一个库存记录,在补偿型事务处理过程中,用80扣减为60,这个时候仓库管理员查询数据结果,看到的是60,之后因为异常回滚,库存回滚到原来的80,那么这个时候库存管理员看到的60,其实就是脏数据,而这个中间状态就是补偿型事务存在的脏数据。

  和补偿型事务不同,XA协议要求事务资源 本身提供对规范和协议的支持,因为事务资源感知并参与分布式事务处理过程中,所以事务资源可以保证从任意视角对数据的访问有效隔离性,满足全局数据的一致性。

  XA的价值

  与 补偿型 不同,XA 协议 要求 事务资源 本身提供对规范和协议的支持。因为 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。比如,刚才提到的库存更新场景,XA 事务处理过程中,中间状态数据库存 50 由数据库本身保证,是不会被仓库管理员的查询统计看到的。除了 全局一致性 这个根本性的价值外,支持 XA 还有如下几个方面的好处: 业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。 数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。 多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少。 传统基于 XA 应用的迁移:传统的,基于 XA 协议的应用,迁移到 Seata 平台,使用 XA 模式将更平滑。

  seata XA基础理论

  前提 支持XA 事务的数据库。 Java 应用,通过 JDBC 访问数据库。

  整体机制 在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。

  image-20220717154316317

  执行阶段 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)

  完成阶段 分支提交:执行 XA 分支的 commit 分支回滚:执行 XA 分支的 rollback

  XA模式只支持实现了XA协议的数据库。Seata支持MySQL、Oracle、PostgreSQL和MariaDB。

  实战示例

  XA事务模式使用和AT事务模式使用基本没有区别,我们还是用前面的例子,只需要在库存和订单微服务的配置文件中添加data-source-proxy-mode为XA,其他不变

  XA模式不需要undo_log表,演示时我们可以先把订单和库存数据库的undo_log表删掉,为了看下xid,增加一个xid打印日志,启动订单和库存微服务,访问http://localhost:4070/order/create/2000/1001/5 ,订单微服务日志打印如下

  image-20220717125832841

  库存微服务信息日志信息也使用XA模式,全面xid事务deduct xid:192.168.5.52:8091:9043512942211306753和订单相同

  image-20220717130228498

  查看订单数据库已经添加订单,库存已经建库存,这个是正常场景。

  image-20220717130352578

  在订单中添加一个异常,http://localhost:4070/order/create/2000/1001/5 显示异常,后台日志打印异常的提示

  image-20220717130830230

  库存微服务中日志已经进行回滚,查看库存表没有减库存,订单也没有创建

  image-20220717131001619

  TCC模式

  一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的: 一阶段 prepare 行为 二阶段 commit 或 rollback 行为

  image-20220712141421195

  根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.

  TCC模式不依赖数据源(1.4.2版本及之前),1.4.2版本之后增加了TCC防悬挂措施,需要数据源支持。

  AT模式是基于 支持本地 ACID 事务 的 关系型数据库 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

  TCC 模式则不依赖于底层数据资源的事务支持,支持把 自定义 的分支事务纳入到全局事务的管理中。 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。 二阶段 commit 行为:调用 自定义 的 commit 逻辑。 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

  案例可以参考https://github.com/seata/seata-samples ,后续再补充实战案例

  Saga模式

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

  image-20220712141902003 使用场景 业务流程长、业务流程多 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口 优势 一阶段提交本地事务,无锁,高性能 事件驱动架构,参与者可异步执行,高吞吐 补偿服务易于实现 缺点 不保证隔离性 Saga模式不依赖数据源。

  Seata基于状态机引擎的 Saga 实现:

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

  注意: 异常发生时是否进行补偿也可由用户自定义决定 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

  Seata Saga 提供了一个可视化的状态机设计器方便用户使用,代码和运行指南请参考: https://github.com/seata/seata/tree/develop/saga/seata-saga-statemachine-designer,官方案例可以参考https://github.com/seata/seata-samples ,后续再充实战案例

  **本人博客网站 **IT小神 www.itxiaoshen.com