Seata 入门教程 - 中级篇(2020 年)

大纲

前言

官方资源

Seata 的四大事务模式

Seata 为用户提供了 AT、TCC、Saga 和 XA 事务模式,致力于提供高性能和简单易用的分布式事务服务。

1、AT 模式

1.1、概述

AT 模式是 Seata 创新的一种非入侵式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层。当我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy 类,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志、检查全局锁等。

1.2、使用前提

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

1.3、整体机制

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

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源

  • 二阶段:

    • 提交异步化,非常快速地完成
    • 回滚是通过一阶段的回滚日志进行反向补偿
1.3.1、写隔离
  • 一阶段本地事务提交前,需要确保先拿到全局锁
  • 拿不到全局锁 ,不能提交本地事务
  • 拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁

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

tx1 先开始,开启本地事务,拿到本地锁,执行更新操作 m = 1000 - 100 = 900。在 tx1 本地事务提交前,先拿到该记录的全局锁,本地提交事务后会释放本地锁。

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 持有的,所以不会出现脏写的问题。

1.3.2、读隔离

在数据库本地事务隔离级别读已提交(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 语句。

1.4、工作机制

1.4.1、一阶段

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

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

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

seata-model-at-1

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

1.4.2、二阶段提交

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

seata-model-at-2

1.4.3、二阶段回滚

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

seata-model-at-3

1.4.4、举例说明

这里以一个示例来说明整个 AT 分支的工作过程。

业务查询逻辑

  • (1) 业务表 product 的结构
FieldTypeKey
idbigint(20)PRI
namevarchar(100)
sincevarchar(100)
  • (2) AT 分支事务的业务查询逻辑:
1
update product set name = 'GTS' where name = 'TXC';

一阶段

  • (1) 解析 SQL:得到 SQL 的类型(update),表(product),条件(where name = 'TXC')等相关的信息。
  • (2) 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
1
select id, name, since from product where name = 'TXC';

得到前镜像:

idnamesince
1TXC2014
  • (3) 执行业务 SQL:更新这条记录的 nameGTS
  • (4) 查询后镜像:根据前镜像的结果,通过主键定位数据。
1
select id, name, since from product where id = 1;

得到后镜像:

idnamesince
1GTS2014
  • (5) 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
{
"branchId": 641789253,
"undoItems": [
{
"afterImage": {
"rows": [
{
"fields": [
{
"name": "id",
"type": 4,
"value": 1
},
{
"name": "name",
"type": 12,
"value": "GTS"
},
{
"name": "since",
"type": 12,
"value": "2014"
}
]
}
],
"tableName": "product"
},
"beforeImage": {
"rows": [
{
"fields": [
{
"name": "id",
"type": 4,
"value": 1
},
{
"name": "name",
"type": 12,
"value": "TXC"
},
{
"name": "since",
"type": 12,
"value": "2014"
}
]
}
],
"tableName": "product"
},
"sqlType": "UPDATE"
}
],
"xid": "xid:xxx"
}
  • (6) 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的全局锁 。
  • (7) 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
  • (8) 将本地事务提交的结果上报给 TC。

二阶段 - 提交

  • (1) 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  • (2) 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

二阶段 - 回滚

  • (1) 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  • (2) 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
  • (3) 数据校验:拿 UNDO LOG 中的后镜像与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理(如转人工处理)。
  • (4) 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的 SQL 语句:
1
update product set name = 'TXC' where id = 1;
  • (5) 提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

回滚日志表的结构

UNDO_LOG 表的结构如下,不同数据库在类型上会略有差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 注意此处0.7.0+ 增加字段 context
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
1.4.5、总结说明
  • 第一阶段:假如我们现在插入或更新一条数据,根据动态代理它会提取你插入或更新前的数据,保存一个原快照,然后再去执行 业务 SQL,再保存一个新快照,生成一个行锁。当你这个业务方法没有执行完,这个锁是不会释放的。最终提交 业务 SQL,业务表和 unlog 表是在同一个本地事务提交中,也就是要么同时成功,要么同时失败。因为你更新或插入一条数据,unlog 表会记录一些原始数据便于回滚,是 Seata 帮助我们实现了回滚。
  • 第二阶段:在这个阶段 Seata 会查看你的日志是否成功,如果成功不会做任何操作,如果失败,它会做一个反向补偿,使用 unlog 表记录一些原数据进行回滚操作。

1.5、优缺点与使用场景

  • 优点:

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

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

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

2、TCC 模式

2.1、概述

TCC 模式是 Seata 支持的一种由业务方细粒度控制的入侵式分布式事务解决方案,是继 AT 模式后第二种支持的事务模式,最早由蚂蚁金服贡献。其分布式事务模型直接作用于服务层,不依赖底层数据库,可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好,可以说是为独立部署的 SOA 服务而设计的。TCC 需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个接口。事务发起方在一阶段执行 Try 操作,在二阶段提交执行 Confirm 操作,二阶段回滚执行 Cancel 操作。TCC 三个接口的描述如下:

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

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

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

seata-tcc-model

2.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 模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

2.3、优缺点与使用场景

  • 优点:

    • 适合微服务化场景
    • 无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多
    • 用户可以自己定义业务的补偿逻辑,由业务层保证事务的一致性
    • TCC 完全不依赖底层数据库,能够实现跨数据库、跨应用资源管理,可以提供给业务方更细粒度的控制
  • 缺点:

    • TCC 模式下开发者需要针对业务系统自行实现 Try、Confirm、Cancel 接口,对业务代码有着非常大的入侵性,设计相对复杂
    • 需要考虑如何将业务模型拆成二阶段,实现成 TCC 的 3 个接口,并且保证 Try 成功 Confirm 就一定能成功,Confirm 失败会不断重试
  • 适用场景:

    • 分布式事务的业务逻辑中除了数据库操作外,包含了其他中间件(如 MQ)的事务逻辑
    • TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景

3、Saga 模式

3.1、概述

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

seata-saga-model

3.2、整体机制

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

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

seata-saga-model-2

3.3、优缺点与使用场景

  • 优点:

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

    • 不保证隔离性
    • 补偿逻辑需要自行实现
  • 适用场景:

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

4、XA 模式

4.1、概述

XA 模式是从 Seata 1.2 版本支持的事务模式。XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。Seata XA 模式是利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。

4.2、使用前提

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

4.3、整体机制

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

seata-xa-model

执行阶段:

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

完成阶段:

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

4.4、工作机制

整体运行机制

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);
}

4.5、优缺点与使用场景

  • 优点

    • XA 模式使用的是二阶段提交(2PC)协议,可以确保分布式事务的强一致性
    • XA 模式和 AT 模式一样,对业务是无侵入的,不会给应用设计和开发带来额外负担
    • XA 协议被主流关系型数据库广泛支持,开发者不需要手动实现分布式事务的逻辑,数据库 XA 驱动和 Seata 会自动完成分布式事务的管理
  • 缺点

    • 二阶段提交(2PC)会带来较高的事务开销,尤其是在事务参与者较多的场景下,可能导致较大的延迟
    • 在事务提交的第一阶段(准备阶段),会锁定资源,可能造成阻塞等待。事务资源长时间得不到释放,锁定周期长,而且在应用层上面无法干预,性能表现较差
    • 要求所有参与者的数据库节点高度可用,如果某个数据库节点不可用,事务可能会被阻塞
  • 适用场景

    • 适用于 AT 模式未适配的数据库应用
    • 适用于想要迁移到 Seata 平台基于 XA 协议的老旧应用
    • 适用于事务较短、并发量较低的场景,可以接受较高的性能开销来保证数据一致性
    • 适用于金融、电信等对一致性要求高的场景,比如银行转账、支付等
    • 适用于跨数据库、跨服务的场景,但需要数据库支持 XA 协议

Seata 的数据库类型支持

  • AT 模式

    • AT 模式支持 MySQL、Oracle、PostgreSQL、MariaDB、TiDB。
  • TCC 模式

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

    • Saga 模式不依赖数据源。
  • XA 模式

    • XA 模式只支持实现了 XA 协议的数据库
    • 比如,XA 模式支持 MySQL、Oracle、PostgreSQL、MariaDB

参考资料