前言
众所周知,分布式事务是个复杂的问题,有很多种不同的思路和方法。
在 Seata 项目中,最早由阿里巴巴中间件开源出的 AT 模式(Automatic Transaction) 是一套创新的、业务无侵入的分布式事务解决方案。截止 Seata 的 GA 版本发布,AT 模式 已经在开源社区引起了广泛关注, 40 余家企业用户已经将 Seata 的 AT 模式应用于生产。
AT 模式 的独特之处到底在哪里?这种事务模式的设计思路来自哪里?价值是什么?
希望可以通过这篇文章,帮助大家深入解读 Seata 的 AT 模式,理解其中的精髓。
AT 产生的根源
我们先从 AT 模式产生的根源谈起。
单体应用的演化
以前我们都是开发单体应用。
最早的形态,简化起来,大体就是:一个数据库,用来存放业务数据;上面写程序,实现业务逻辑。这个黄色的点,就是数据访问的入口,或者叫数据源。
单体应用业务逻辑也会变复杂,我们可能要划分若干不同的模块。
数据层面也可能需要根据领域来划分成不同的业务域,分开进行分析、设计和管理。
但是,无论怎么演化,我们看到,只要黄色的点——数据入口是一个,那么,数据一致性问题,我们就不必关心,数据库的事务能力完全可以帮我们解决。
分布式架构带来的挑战
进入分布式架构阶段,情况就有变化了。
我们应用架构可能会从数据和服务两个维度进行切分:
数据库容量,达到瓶颈,拆分成多库多表。
业务层面做微服务化,从单体应用拆分为若干独立的、分布化的服务。
最典型的例子可能就是阿里。阿里大概是国内最早一批做分布化改造企业,中间件团队提供两大利器来支撑业务做这个事情:
- 一个是 HSF:解决应用这一层的问题。
- 一个是 TDDL:解决数据这一层的问题。
这两个中间件产品都做得非常好。但与此同时,我们会发现一个以前不是问题的问题浮现出来了。看图里这么多黄色的点,涉及跨数据库、跨服务的这么多数据入口,这种情况下,数据一致性的问题,怎么解决呢?我们需要一个分布式的中间件,来解决分布化带给我们新的挑战。
这个新的挑战就是 Seata 的 AT 模式 产生的背景:早在 2014 年,阿里中间件就在分布式事务中间件产品 TXC(即之后的阿里云 GTS:https://www.aliyun.com/aliware/txc)中创造并实践了 AT 模式 在分布式架构下的应用。
分布式事务需求
对于分布式事务中间件的需求,总结起来是 3 个大的方面,每个方面 2 个小的角度。
首先,编程模型。
HSF 和 TDDL 这两个中间件最大的价值就在于,他们把服务分布化和数据分片的技术复杂性透明化了。他们带给应用的编程模型是保持不变的,应用的设计和编码完全不需要意识到:我调用的是本地还是远程的服务?我后面的数据是否是分库分表的?
在这个基础上,分布式事务中间件,也不应该为了解决事务问题,而破坏既有的编程模型。中间件不应该给应用层面带来更多设计和编码上的负担。
这里面有两个角度:
- 从服务提供者的角度:不侵入业务。业务的设计不考虑,是否是运行在分布式事务场景下。
- 从调用者的角度:确定的一致性。调用一个有分布式事务支持的服务,应该像本地事务一样,在这个调用完成后,无论是正常结束,还是抛出异常失败掉,调用者根据调用结果,可以得到一个数据一致性的 确定结果,而不是一个中间态,最终一致的结果。
简单来说:事务技术的根本目标,就是要帮助应用的开发 简化编程模型。设想,在一个没有事务支持的数据库上工作,实现一个业务上原子逻辑,不是做不到,是会让编程模型非常复杂而已。所以,保持编程模型的基本不变,是对事务中间件的重要需求。
第二点,性能。
这点容易理解,业务应用对性能肯定是有一个基本要求的,加入分布式事务的支持后,不能让应用的性能有显著的下降,以至于达不到业务上的需求底线,这是不能接受的。
保障性能,简单来看,就是两个角度:
- 一方面:显然,与没有事务支持的业务比较,加入事务支持势必会引入一些额外的开销。这个角度就最直接的,少做事情,尽量少的增加开销。
- 另一方面:衡量一个事务处理系统(TP)性能的主要指标是系统的 吞吐。加入事务支持后,业务的 RT 势必会提高,要使整个系统保持吞吐的话,就要求能允许更多的并发。即,尽量减少分布式事务制约并发的因素。
最后,可用性。
两个角度:
- 第一个角度:提供分布式事务支持的中间件本身,要高可用,这个好理解,没什么好说的。
- 第二个角度:从整个业务系统的角度来看,在极端情况下,如果分布式事务中间件出了问题,不能马上恢复,形成部分事务不能正常结束的局面。这个时候,线上业务的可用性较之于数据一致性来说,可能要重要得多。部分数据不致,大不了,后面人工校对来补救。但线上业务不可用对业务来说,可能是致命的。所以,要求能支持 降级,也就是暂时放弃分布式事务支持,让业务 裸奔。
Why NOT XA?
基于上面这 3 个大的方面的需求,我们考虑怎么做?
分布式事务的解决方案有很多选择,但基于编程模型不变的需求,我们实际面临的选择就不多了。
最直接的选择就是 XA,Seata AT 模式的核心思路也是从审视 XA 的问题开始的。
如图,这是一个抽象出来的典型的分布式事务链路,3 个服务,分别有自己的数据库,服务调用形成一个业务链路,这个链路的数据要保障数据一致性。
如果用 XA 协议来支持分布式事务,会什么样?我们看一下:
- 数据源(图中绿色的圈),要求用 XA 数据源。
- SQL 执行的前后,需要做 xa start 和 xa end 。
- 提交阶段,XA 的两阶段提交,先调用各个参与方的 prepare,再根据结果,调用参与方的 commit 或 rollback。
首先,上述这些工作,对于业务来说,都是额外的开销。多轮的交互,这个开销是不小的。
其次,我们注意到,我这里画了一些锁。分别在两个地方涉及到锁:
- 一个是数据,这好理解,XA 事务过程中,数据是被锁定的。
- 另一方面是连接,XA 事务过程中,连接也是被锁定的。至少在两阶段提交的 prepare 之前,连接是不能释放的(因为连接断开,这个连接上的 XA 分支就会回滚,整个事务也会被迫回滚)。
较之于数据的锁定(数据的锁定对于事务的隔离性是必要的机制),连接的锁定带给整个业务系统的直接影响就是,限制了并发度。
为了便于理解,我们假设 3 个服务各自只有一个连接资源。
- 没有 XA 的事务支持时,3 个线程跑业务:一个服务执行完,连接就可以给另外一个事务用。
- 而使用 XA 协议来支持时,同样条件,3 个线程,就只能因为连接的限制,排起队来了。
这可能是我们普遍认为 XA 性能问题的主要原因。
另外,还有一点,就是 数据的锁定:
XA 的数据锁定是在 数据库内部机制 维护的,所以在极端异常情况下,我们如果不直接让 DBA 干预数据库去解除数据锁定,我们是无法做到让业务降级保持可用的。
综合上面一系列的分析,针对分布式事务中间件在新的背景下的新的需求,我们需要一个不同的答案,这个答案就是 Seata 的 AT 模式。
AT 的原理和机制
核心思路
对比之前对 XA 分析,AT 的核心出发点,就是 斩断 XA 可能带来的制约。
还是看这个微服务场景下的分布式事务模型:
- 调用链路上的 SQL 操作,当前服务调用完成后,直接 提交,释放资源(连接和本地事务的数据锁定)。
- 业务数据提交的同时,把数据的 回滚日志 一并存放到回滚日志表里,以备全局回滚时使用。
这里面有两个关键:
第一,利用了 数据库本地事务 的特性,让回滚日志和业务数据的写入保证原子性:只要有业务数据提交成功,就一定有相应的回滚日志数据。
第二,考虑到实际业务的运行过程,绝大部分情况下最终是成功全局提交的。直接本地提交的机制,省去了绝大部分情况下,两阶段提交的开销。
理解核心思路后,大家肯定还有一些疑问,比如:回滚日志如何生成?我们接着往下,后面会讲到。我们先来看一下 Seata 的架构。
Seata 的架构
3 个组件:TM(Transaction Manager)、RM(Resource Manager) 和 TC(Transaction Coordinator)。一个典型的事务过程:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- XID 在微服务调用链路的上下文中传播。
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议。
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
事务模型和模式
基于架构上定义的 3 个角色,Seata 把分布式事务抽象成这样一个 模型。
TM 定义全局事务的边界。
RM 负责定义分支事务的边界和行为。
TC 跟 TM 和 RM 交互(开启、提交、回滚全局事务;分支注册、状态上报和分支的提交、回滚),做全局的协调。
所谓 Seata 的 事务模式,准确地讲,应该是这个框架下,RM 驱动的 分支事务的不同行为模式,应该是 事务(分支)模式。
放到事务模式中来看,AT 模式,就是如图所示的这个样子,其分支事务的行为模式为:
- 在业务执行的同时,根据业务数据操作的具体行为,自动生成回滚日志,记录在回滚日志表里。
- 在全局事务回滚时,根据回滚日志,自动生成并执行补偿回滚的数据操作。
- 在全局事务提交时,异步进行回滚日志的自动清理,事务得以马上结束。
具体的实现机制
下面,我们来看 AT 模式的具体实现机制。
首先,应用需要使用 Seata 的 JDBC 数据源代理(也就是 AT 模式的 RM)。
其次,一个符合 Seata 事务模型的分布式事务,分为两个大的阶段:执行阶段 和 完成阶段。
执行阶段:
Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。
这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在。
基于这样的机制,分支的本地事务便可以在全局事务的 执行阶段 提交,马上释放本地事务锁定的资源。
完成阶段:
- 如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),完成阶段 可以非常快速地结束。
- 如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
隔离性
讲到这里,关于 AT 模式大部分问题我们应该都清楚了,但总结起来,核心也只解决了一件事情,就是 ACID 中最基本、最重要的 A(原子性)。
但是,光解决 A 显然是不够的:既然本地事务已经提交,那么如果数据在全局事务结束前被修改了,回滚时怎么处理?ACID 的 I(隔离性)在 Seata 的 AT 模式是如何处理的呢?
Seata AT 模式引入 全局锁 机制来实现隔离。
全局锁 是由 Seata 的 TC 维护的,事务中涉及的数据的锁。
写隔离
- 执行阶段 本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。
tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 完成阶段 全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的 完成阶段 全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
集中管理全局锁的考虑
全局锁是由 TC 也就是 server 来集中维护,而不是在数据库维护的。这样做有两点好处:
- 一方面:锁的释放非常快,尤其是在全局提交的情况下,收到全局提交的请求,锁马上就释放掉了,不需要与 RM 或数据库进行一轮交互。
- 另外一方面:因为锁不是数据库维护的,从数据库层面看,数据没有锁定。这也就是给极端情况下,业务 降级 提供了方便,事务协调器异常导致的一部分异常事务,不会 block 后面业务的继续进行。
实例
以一个示例来说明整个 AT 模式分支的工作过程。
业务表:product
Field | Type | Key |
---|---|---|
id | bigint(20) | PRI |
name | varchar(100) | |
since | varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
执行阶段
过程:
- 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。
- 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';
得到前镜像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
- 执行业务 SQL:更新这条记录的 name 为 'GTS'。
- 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1`;
得到后镜像:
id | name | since |
---|---|---|
1 | GTS | 2014 |
- 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到
UNDO_LOG
表中。
{
"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"
}
- 提交前,向 TC 注册分支:申请
product
表中,主键值等于 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 记录。
和 XA 的关系
前面一直拿 AT 和 XA 做比较。
这里特别说明一下,并不是说 XA 协议本身有问题,只是说在某些场景的需求下,基于 XA 做不理想。
但同样,另外还有一些对内外部 一致性要求非常高 的场景,可能 XA 又是非常适合,甚至必需的。
这也是接下来 Seata 将提供 XA 模式的原因。
关于 XA 模式 这里就不展开了,后面会有专门的文章和大家交流。
AT 的核心价值
AT 模式到底带给我们什么价值呢?
首先,从技术原理角度来看,非常重要的一点是:平衡。
必须承认,分布式事务是个复杂的问题,目前还没有任何一种解决方案可以非常完美地适应所有应用场景。
如果把分布式事务方案按 一致性、性能 和 易用性 这 3 个维度来考量:AT 模式,实际上是在业务需求允许的前提下,找到一个比较好的平衡点。
编程模型不做改变的前提下,达到确定的一致性,而且保证了性能和系统可用性。
其次,从用户的角度来看。我们设想一个企业业务的成长过程:
1.0:单体应用,快速上线,这个时候完全不涉及分布式事务。
2.0:单个数据库无法支撑,数据分布到多个数据库,产生分布式事务问题。
3.0:微服务化,进一步产生跨服务的分布式事务。
4.0:跨应用的整合,成为 SaaS 或 FaaS 的平台,在更大的范围,产生分布式事务问题。
基于 Seata 的 AT 模式构建企业业务的分布式事务解决方案,可以带来以下 3 个方面的 核心价值:
- 低成本: 编程模型 不变,轻依赖 不需要为分布式事务场景做特定设计,业务像搭积木一样自然地构建成长。
- 高性能:协议 不阻塞;资源释放快,保证业务的吞吐。
- 高可用:极端的异常情况下,可以暂时 跳过异常事务,保证整个业务系统的高可用。
AT 的现在和未来
没有 银蛋,AT 模式带来上面提到的价值的同时,也必定有一些局限和不足。
较重的 SDK
AT 模式有很大一部分功能依赖于 SDK 的实现,包括 SQL 解析、回滚日志的生成、分支提交回滚逻辑的执行等等。
这些关键运行机制是基于 Java 的 JDBC 构建起来的。如果要支持其他语言,迁移成本非常高。
面向云原生时代,AT 模式未来的方向将是 SDK 的轻量化和标准化,把大部分能力下沉到代理层(Agent 或 Sidecar 的形式),让应用只需要很简单的 SDK 和标准的 SQL 就可以工作。
能力边界
从工作原理来看,AT 模式有一些特定的使用条件和局限。
首先,AT 模式的 基本条件 是:数据库本身必须支持 本地事务。AT 的基本工作机制是基于本地事务的。
其次,数据表必须定义 主键。回滚日志的生成和使用,是基于数据主键的。
另外,隔离性,这也是所有基于 补偿 的分布式事务解决方案,都面临的问题:隔离性很难做到很高,或者说,要做到较高隔离性的成本和收益是不匹配的。
基于这些目前的局限,Seata 项目整体的应对策略是,提供各类不同的事务模式来取长补短,实现全场景的覆盖。
目前已经具备和正在规划中的,一共是两大类,4 种事务模式:
业务无侵入的:AT、XA
业务侵入的:TCC、Saga
这些模式各自有其适用和不适用的场景,Seata 将把这些模式很好地融合起来,给用户提供一站式的解决方案。
总结
Seata 的 AT 模式是分布式架构演进过程中,分布式事务中间件在阿里巴巴实践的创造性解决方案。
Seata 的 AT 模式基于本地事务的特性,通过拦截并解析 SQL 的方式,记录自定义的回滚日志,从而打破 XA 协议阻塞性的制约,在一致性、性能、易用性 3 个方面取得平衡:在达到确定一致性(非最终一致)的前提下,即保障较高的性能,又能完全不侵入业务。
在绝大部分应用场景下,Seata 的 AT 模式都能很好地发挥作用,把应用的分布式事务支持成本降到极低的水平。
对于一些不适用 AT 模式的场景,Seata 也提供其他几类主流的分布式事务解决方案来补齐。
附录
- Seata 官网:http://seata.io/zh-cn/
- Seata on GitHub:https://github.com/seata/seata
- 支持 Seata AT 模式的阿里云 GTS(Global Transaction Service):https://www.aliyun.com/aliware/txc