转载

带你读透 SEATA 的 AT 模式

前言

众所周知,分布式事务是个复杂的问题,有很多种不同的思路和方法。

在 Seata 项目中,最早由阿里巴巴中间件开源出的 AT 模式(Automatic Transaction) 是一套创新的、业务无侵入的分布式事务解决方案。截止 Seata 的 GA 版本发布,AT 模式 已经在开源社区引起了广泛关注, 40 余家企业用户已经将 Seata 的 AT 模式应用于生产。

AT 模式 的独特之处到底在哪里?这种事务模式的设计思路来自哪里?价值是什么?

希望可以通过这篇文章,帮助大家深入解读 Seata 的 AT 模式,理解其中的精髓。

AT 产生的根源

我们先从 AT 模式产生的根源谈起。

单体应用的演化

以前我们都是开发单体应用。

file

Monolithic

最早的形态,简化起来,大体就是:一个数据库,用来存放业务数据;上面写程序,实现业务逻辑。这个黄色的点,就是数据访问的入口,或者叫数据源。

单体应用业务逻辑也会变复杂,我们可能要划分若干不同的模块。

数据层面也可能需要根据领域来划分成不同的业务域,分开进行分析、设计和管理。

file

Monolithic+

但是,无论怎么演化,我们看到,只要黄色的点——数据入口是一个,那么,数据一致性问题,我们就不必关心,数据库的事务能力完全可以帮我们解决。

分布式架构带来的挑战

进入分布式架构阶段,情况就有变化了。

file

Distributed Applications

我们应用架构可能会从数据和服务两个维度进行切分:

  • 数据库容量,达到瓶颈,拆分成多库多表。
  • 业务层面做微服务化,从单体应用拆分为若干独立的、分布化的服务。

file

Microservices

最典型的例子可能就是阿里。阿里大概是国内最早一批做分布化改造企业,中间件团队提供两大利器来支撑业务做这个事情:

  • 一个是 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 的问题开始的。

file

Demo Business in Microservices

如图,这是一个抽象出来的典型的分布式事务链路,3 个服务,分别有自己的数据库,服务调用形成一个业务链路,这个链路的数据要保障数据一致性。

如果用 XA 协议来支持分布式事务,会什么样?我们看一下:

file

How Does XA Do
  • 数据源(图中绿色的圈),要求用 XA 数据源。
  • SQL 执行的前后,需要做 xa start 和 xa end 。
  • 提交阶段,XA 的两阶段提交,先调用各个参与方的 prepare,再根据结果,调用参与方的 commit 或 rollback。

首先,上述这些工作,对于业务来说,都是额外的开销。多轮的交互,这个开销是不小的。

其次,我们注意到,我这里画了一些锁。分别在两个地方涉及到锁:

  • 一个是数据,这好理解,XA 事务过程中,数据是被锁定的。
  • 另一方面是连接,XA 事务过程中,连接也是被锁定的。至少在两阶段提交的 prepare 之前,连接是不能释放的(因为连接断开,这个连接上的 XA 分支就会回滚,整个事务也会被迫回滚)。

较之于数据的锁定(数据的锁定对于事务的隔离性是必要的机制),连接的锁定带给整个业务系统的直接影响就是,限制了并发度。

为了便于理解,我们假设 3 个服务各自只有一个连接资源。

file

Throughput Without Transaction Support
  • 没有 XA 的事务支持时,3 个线程跑业务:一个服务执行完,连接就可以给另外一个事务用。

file

Throughput With XA
  • 而使用 XA 协议来支持时,同样条件,3 个线程,就只能因为连接的限制,排起队来了。

    这可能是我们普遍认为 XA 性能问题的主要原因。

    另外,还有一点,就是 数据的锁定

    XA 的数据锁定是在 数据库内部机制 维护的,所以在极端异常情况下,我们如果不直接让 DBA 干预数据库去解除数据锁定,我们是无法做到让业务降级保持可用的。

    综合上面一系列的分析,针对分布式事务中间件在新的背景下的新的需求,我们需要一个不同的答案,这个答案就是 Seata 的 AT 模式

AT 的原理和机制

核心思路

对比之前对 XA 分析,AT 的核心出发点,就是 斩断 XA 可能带来的制约。

file

Original Idea of Seata-AT

还是看这个微服务场景下的分布式事务模型:

  • 调用链路上的 SQL 操作,当前服务调用完成后,直接 提交,释放资源(连接和本地事务的数据锁定)。
  • 业务数据提交的同时,把数据的 回滚日志 一并存放到回滚日志表里,以备全局回滚时使用。

这里面有两个关键:

第一,利用了 数据库本地事务 的特性,让回滚日志和业务数据的写入保证原子性:只要有业务数据提交成功,就一定有相应的回滚日志数据。

第二,考虑到实际业务的运行过程,绝大部分情况下最终是成功全局提交的。直接本地提交的机制,省去了绝大部分情况下,两阶段提交的开销

理解核心思路后,大家肯定还有一些疑问,比如:回滚日志如何生成?我们接着往下,后面会讲到。我们先来看一下 Seata 的架构。

Seata 的架构

file

Seata Architecture

3 个组件:TM(Transaction Manager)、RM(Resource Manager) 和 TC(Transaction Coordinator)。一个典型的事务过程:

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
  2. XID 在微服务调用链路的上下文中传播。
  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议。
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

事务模型和模式

基于架构上定义的 3 个角色,Seata 把分布式事务抽象成这样一个 模型

file

Seata Defined Distributed Transaction Model

TM 定义全局事务的边界。

RM 负责定义分支事务的边界和行为。

TC 跟 TM 和 RM 交互(开启、提交、回滚全局事务;分支注册、状态上报和分支的提交、回滚),做全局的协调。

所谓 Seata 的 事务模式,准确地讲,应该是这个框架下,RM 驱动的 分支事务的不同行为模式,应该是 事务(分支)模式

放到事务模式中来看,AT 模式,就是如图所示的这个样子,其分支事务的行为模式为:

  • 在业务执行的同时,根据业务数据操作的具体行为,自动生成回滚日志,记录在回滚日志表里。
  • 在全局事务回滚时,根据回滚日志,自动生成并执行补偿回滚的数据操作。
  • 在全局事务提交时,异步进行回滚日志的自动清理,事务得以马上结束。

具体的实现机制

下面,我们来看 AT 模式的具体实现机制。

首先,应用需要使用 Seata 的 JDBC 数据源代理(也就是 AT 模式的 RM)。

file

Data Source Proxy

其次,一个符合 Seata 事务模型的分布式事务,分为两个大的阶段:执行阶段完成阶段

执行阶段:

Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。

这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在。

file

Branch Transaction with UNDO LOG

基于这样的机制,分支的本地事务便可以在全局事务的 执行阶段 提交,马上释放本地事务锁定的资源。

完成阶段:

  • 如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),完成阶段 可以非常快速地结束。

file

Global Commit
  • 如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

file

Global Rollback

隔离性

讲到这里,关于 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 需要重试等待 全局锁

file

Write-Isolation: Commit

tx1 完成阶段 全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

file

Write-Isolation: Rollback

如果 tx1 的 完成阶段 全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

file

Read Isolation: 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';

执行阶段

过程:

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

得到前镜像:

id name since
1 TXC 2014
  1. 执行业务 SQL:更新这条记录的 name 为 'GTS'。
  2. 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1`;

得到后镜像:

id name since
1 GTS 2014
  1. 插入回滚日志:把前后镜像数据以及业务 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"
}
  1. 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁
  2. 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
  3. 将本地事务提交的结果上报给 TC。

完成阶段-回滚

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

完成阶段-提交

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

和 XA 的关系

前面一直拿 AT 和 XA 做比较。

这里特别说明一下,并不是说 XA 协议本身有问题,只是说在某些场景的需求下,基于 XA 做不理想。

但同样,另外还有一些对内外部 一致性要求非常高 的场景,可能 XA 又是非常适合,甚至必需的。

这也是接下来 Seata 将提供 XA 模式的原因。

关于 XA 模式 这里就不展开了,后面会有专门的文章和大家交流。

AT 的核心价值

AT 模式到底带给我们什么价值呢?

首先,从技术原理角度来看,非常重要的一点是:平衡

file

Balance

必须承认,分布式事务是个复杂的问题,目前还没有任何一种解决方案可以非常完美地适应所有应用场景。

如果把分布式事务方案按 一致性性能易用性 这 3 个维度来考量:AT 模式,实际上是在业务需求允许的前提下,找到一个比较好的平衡点。

编程模型不做改变的前提下,达到确定的一致性,而且保证了性能和系统可用性。

其次,从用户的角度来看。我们设想一个企业业务的成长过程:

file

Demo Business Growth
  • 1.0:单体应用,快速上线,这个时候完全不涉及分布式事务。
  • 2.0:单个数据库无法支撑,数据分布到多个数据库,产生分布式事务问题。
  • 3.0:微服务化,进一步产生跨服务的分布式事务。
  • 4.0:跨应用的整合,成为 SaaS 或 FaaS 的平台,在更大的范围,产生分布式事务问题。

基于 Seata 的 AT 模式构建企业业务的分布式事务解决方案,可以带来以下 3 个方面的 核心价值

  • 低成本: 编程模型 不变,轻依赖 不需要为分布式事务场景做特定设计,业务像搭积木一样自然地构建成长。
  • 高性能:协议 不阻塞;资源释放快,保证业务的吞吐。
  • 高可用:极端的异常情况下,可以暂时 跳过异常事务,保证整个业务系统的高可用。

AT 的现在和未来

没有 银蛋,AT 模式带来上面提到的价值的同时,也必定有一些局限和不足。

较重的 SDK

file

Heavy SDK

AT 模式有很大一部分功能依赖于 SDK 的实现,包括 SQL 解析、回滚日志的生成、分支提交回滚逻辑的执行等等。

这些关键运行机制是基于 Java 的 JDBC 构建起来的。如果要支持其他语言,迁移成本非常高。

面向云原生时代,AT 模式未来的方向将是 SDK 的轻量化和标准化,把大部分能力下沉到代理层(Agent 或 Sidecar 的形式),让应用只需要很简单的 SDK 和标准的 SQL 就可以工作。

file

Light SDK

能力边界

从工作原理来看,AT 模式有一些特定的使用条件和局限。

首先,AT 模式的 基本条件 是:数据库本身必须支持 本地事务。AT 的基本工作机制是基于本地事务的。

其次,数据表必须定义 主键。回滚日志的生成和使用,是基于数据主键的。

另外,隔离性,这也是所有基于 补偿 的分布式事务解决方案,都面临的问题:隔离性很难做到很高,或者说,要做到较高隔离性的成本和收益是不匹配的。

基于这些目前的局限,Seata 项目整体的应对策略是,提供各类不同的事务模式来取长补短,实现全场景的覆盖。

目前已经具备和正在规划中的,一共是两大类,4 种事务模式:

file

Landscape of Seata
  • 业务无侵入的:AT、XA
  • 业务侵入的:TCC、Saga

这些模式各自有其适用和不适用的场景,Seata 将把这些模式很好地融合起来,给用户提供一站式的解决方案。

总结

Seata 的 AT 模式是分布式架构演进过程中,分布式事务中间件在阿里巴巴实践的创造性解决方案。

Seata 的 AT 模式基于本地事务的特性,通过拦截并解析 SQL 的方式,记录自定义的回滚日志,从而打破 XA 协议阻塞性的制约,在一致性、性能、易用性 3 个方面取得平衡:在达到确定一致性(非最终一致)的前提下,即保障较高的性能,又能完全不侵入业务。

在绝大部分应用场景下,Seata 的 AT 模式都能很好地发挥作用,把应用的分布式事务支持成本降到极低的水平。

对于一些不适用 AT 模式的场景,Seata 也提供其他几类主流的分布式事务解决方案来补齐。

附录

作者:sharajava
链接:https://www.jianshu.com/p/0ed828c2019a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
正文到此结束
本文目录