Seata 入门教程 - 中级篇

1、Seata 整体框架

1.1、Seata 概述

Seata 是一套一站式分布式事务解决方案,为用户提供了 AT、TCC、SAGA 和 XA 事务模式,致力于提供高性能和简单易用的分布式事务服务。

1.2、Seata 的三大模块

Seata 中有三大模块,分别是 TM、RM 和 TC,其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。

seata-modules

  • TC:Transaction Coordinator 事务协调器,维护全局和分支事务的状态,负责协调并驱动全局事务的提交或回滚
  • TM:Transaction Manager 事务管理器,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
  • RM:Resource Manager 资源管理器,管理分支事务处理的资源,向 TC 注册分支事务,上报分支事务的状态,接受 TC 的命令来提交或者回滚分支事务

1.3、Seata 的执行流程

  • TM 开启分布式事务(TM 向 TC 注册全局事务记录)
  • 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 )
  • TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交 / 回滚分布式事务)
  • TC 汇总事务信息,决定分布式事务是提交还是回滚
  • TC 通知所有 RM 提交 / 回滚 资源,事务二阶段结束

2、AT 模式

2.1、前提

  • Java 应用,通过 JDBC 访问数据库
  • 基于支持本地 ACID 事务的关系型数据库

2.2、写隔离

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

举例说明:两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁,本地提交事务释放本地锁。tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。tx2 本地事务提交前,尝试拿该记录的全局锁;tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁 。

seata-model-at-4

如果 tx1 二阶段全局提交,释放全局锁,tx2 拿到全局锁后提交本地事务

seata-model-at-5

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支事务的回滚。此时,如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支事务回滚会失败。tx1 的分支事务回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃等待全局锁并回滚本地事务释放本地锁,tx1 的分支事务最终回滚成功。因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会出现脏写的问题。

2.3、读隔离

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

seata-model-at-6

SELECT FOR UPDATE 语句的执行会申请全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到拿到全局锁,即读取的相关数据是已提交的才返回。出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATESELECT 语句。

2.4、整体机制

AT 模式本质是两阶段提交协议(2PC)的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
  • 二阶段:
    • 提交异步化,非常快速地完成
    • 回滚是通过一阶段的回滚日志进行反向补偿

一阶段:

在一阶段,Seata 会拦截业务 SQL

  • 1)首先解析 SQL 语义,找到业务 SQL 要更新的业务数据,在业务数据被更新前,将其保存成 before image
  • 2)执行 业务 SQL 更新业务数据,在业务数据更新之后,再将其保存成 after image
  • 3)最后生成行锁

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性,任何提交的业务数据的更新一定有相应的回滚日志存在

seata-model-at-1

基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源;这也是 Seata 和 XA 事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对 undolog 中对应数据并反解析成 SQL 来达到回滚目的。同时 Seata 通过代理数据源将业务 SQL 的执行解析成 undolog 来与业务数据的更新同时入库,达到了对业务无侵入的效果。

二阶段提交:

二阶段如果是提交的话,因为 业务 SQL 在一阶段已经提交至数据库,所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

seata-model-at-2

二阶段回滚:

二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 业务 SQL 来还原业务数据。回滚方式便是用 before image 还原业务数据;但在还原前要首先要校验脏写,对比 数据库当前业务数据after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要根据配置策略来做处理(如转人工处理)

seata-model-at-3

通俗讲:

  • 第一阶段:假如我们现在插入或更新一条数据,根据动态代理它会提取你插入或更新的数据,保存一个原快照,然后再去执行 业务 SQL,再保存一个新快照,生成一个行锁。当你这个业务方法没有执行完,这个锁是不会释放的。最终提交 业务 SQL,业务表和 unlog 表是在同一个本地事务中,也就是要么同时成功,要么同时失败。因为你更新或插入一条数据,unlog 表会记录一些原始数据便于回滚,是 Seata 帮助我们实现了回滚
  • 第二阶段:在这个阶段 Seata 会查看你的日志是否成功,如果成功不会做任何操作,如果失败,它会做一个反向补偿,使用 unlog 表记录一些原数据进行回滚操作

2.5、适用场景与优缺点

适用场景:

分布式事务的业务逻辑中仅仅是纯数据库操作,不包含其他中间件的事务逻辑

优点:

改动及代码侵入最小,由 Seata 来负责 Commit 和 Rollback 的自动化提交或回滚操作

缺点:

  • 如果事务中包含缓存存储或发送 MQ 消息等,则不适合使用
  • 多次对数据库操作,以及全局行锁的存在对并发处理性能有影响
  • 为了保证镜像 SQL 的可靠性,需要用户对 SQL 尽量做简化,建议做法:将多条 SQL 语句分解为多个事务中的原子步骤(对应 Seata AT 模式的分支 Branch 概念),如果单条 SQL 语句跨表,也分解成为多个事务中的原子步骤(尽量降低 Seata 存储前 SQL 镜像结果时的风险)

3、TCC 模式

3.1、概述

该模式由蚂蚁金服贡献,TCC 需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个接口。事务发起方在一阶段执行 Try 操作,在二阶段提交执行 Confirm 操作,二阶段回滚执行 Cancel 操作。TCC 三个接口的描述如下:

  • Try:资源的检测和预留
  • Confirm:执行的业务操作提交,要求 Try 成功 Confirm 就一定要能成功
  • Cancel:预留资源释放

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

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为

seata-tcc-model

3.2、AT 与 TCC 的区别

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

AT 模式基于支持本地 ACID 事务的关系型数据库:

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录
  • 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志
  • 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑

所谓的 Seata TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中

3.3、适用场景与优缺点

适用场景:

  • 分布式事务的业务逻辑中除了数据库操作外,包含其他中间件事务逻辑

优点:

  • 适合微服务化场景
  • 无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多
  • 用户可以自己定义业务的补偿逻辑,由业务层保证事务的一致性

缺点:

  • TCC 模式下开发者需要自行实现 Try、Confirm、Cancel 接口,对业务代码有一定的侵入性
  • 需要考虑如何将业务模型拆成 2 阶段,实现成 TCC 的 3 个方法,并且保证 Try 成功 Confirm 就一定能成功,Confirm 失败会不断重试

4、Saga 模式

4.1、概述

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

seata-saga-model

4.2、整机机制

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

  • 1)通过状态图来定义服务调用的流程,并生成 Json 状态语言定义文件
  • 2)状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
  • 3)状态图 Json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚(注意:异常发生时是否进行补偿也可由用户自定义决定)
  • 4)可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

seata-saga-model-2

4.3、适用场景与优缺点

适用场景:

  • 对数据隔离性要求不高,对性能要求高的场景
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
  • 业务流程长、业务流程多、不需马上返回最终结果,只要保证最终一致性的场景

优点:

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

缺点:

  • 不保证隔离性
  • 补偿逻辑需要自行实现

5、XA 模式

5.1、前提

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

5.2、整体机制

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

seata-xa-model

执行阶段:

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

完成阶段:

  • 分支提交:执行 XA 分支的 Commit
  • 分支回滚:执行 XA 分支的 Rollback

5.3、工作机制

整体运行机制:

seata-xa-model-2

XA 模式 运行在 Seata 定义的事务框架内:

  • 执行阶段(E xecute):XA start/XA end/XA prepare + SQL + 注册分支
  • 完成阶段(F inish):XA commit/XA rollback

数据源代理:

XA 模式需要依赖 XAConnection,获取 XAConnection 两种方式:

  • 方式一:要求开发者配置 XADataSource,给开发者增加了认知负担,需要为 XA 模式专门去学习和使用 XA 数据源,与透明化 XA 编程模型的设计目标相违背
  • 方式二:根据开发者的普通 DataSource 来创建,对开发者比较友好,和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可

Seata 优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection,类比 AT 模式的数据源代理机制(如下):

seata-xa-model-3

但是第二种方法有局限:无法保证兼容的正确性。实际上,这种方法是在做数据库驱动程序要做的事情;不同的厂商、不同版本的数据库驱动实现机制是厂商私有的,Seata 只能保证在充分测试过的驱动程序上是正确的,开发者使用的驱动程序版本差异很可能造成机制的失效,这点在 Oracle 上体现非常明显。


综合考虑,XA 模式的数据源代理设计需要同时支持第一种方式:基于 XA 数据源进行代理,类比 AT 模式的数据源代理机制(如下):

seata-xa-model-4

分支注册:

XA Start 需要 Xid 参数,这个 Xid 需要和 Seata 全局事务的 XID 和 BranchId 关联起来,以便由 TC 驱动 XA 分支的提交或回滚。目前 Seata 的 BranchId 是在分支注册过程,由 TC 统一生成的,所以 XA 模式分支注册的时机需要在 XA start 之前。Seata 的 XA 模式将来一个可能的优化方向:把分支注册尽量延后。类似 AT 模式在本地事务提交之前才注册分支,避免分支执行失败情况下,没有意义的分支注册。这个优化方向需要 BranchId 生成机制的变化来配合,即 BranchId 不通过分支注册过程生成,而是生成后再带着 BranchId 去注册分支。

XA 模式的使用:

从编程模型上,XA 模式与 AT 模式保持完全一致,只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换,示例代码如下:

1
2
3
4
5
6
7
8
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);

// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}

6、参考文献