分类
技术面及基本面分析

使用 XA 交易時的指導方針和限制

思考: 协议的阻塞机制本身并不是问题,关键问题在于 协议阻塞 遇上 数据锁定。 如果一个参与全局事务的资源 “失联” 了(收不到分支事务结束的命令),那么它锁定的数据,将一直被锁定。进而,甚至可能因此产生死锁。 这是 使用 XA 交易時的指導方針和限制 XA 协议的核心痛点,也是 Seata 引入 XA 模式要重点解决的问题。 基本思路是两个方面:避免 “失联” 和 增加 “自解锁” 机制。(这里涉及非常多技术细节,暂时不展开,在后续 XA 模式演进过程中,会专门拿出来讨论)

XA 分布式事务原理

在开发中,为了降低单点压力,通常会根据业务情况进行分表分库,将表分布在不同的库中(库可能分布在不同的机器上)。在这种场景下,事务的提交会变得相对复杂,因为多个节点(库)的存在,可能存在部分节点提交失败的情况,即事务的 ACID 特性需要在各个不同的 数据库实例中保证。比如更新 db1 使用 XA 交易時的指導方針和限制 使用 XA 交易時的指導方針和限制 库的 A 表时,必须同步更新 db2 库的 B 表,两个更新形成一个事务,要么都成功,要么都失败。
那么我们如何利用MySQL实现分布式数据库的事务呢?

  • 资源管理器( resource manager ) :用来管理系统资源,是通向事务资源的途径。数据库就是一种资源管理器。资源管理还应该具有管理事务提交或回滚的能力。
  • 事务管理器( transaction manager ): 事务管理器是分布式事务的核心管理者。事务管理器与每个资源管理器( resource
    manager )进行通信,协调并完成事务的处理。事务的各个分支由唯一命名进行标识。

mysql 在执行分布式事务(外部 XA )的时候, mysql 服务器相当于 xa 事务资源管理器,与 mysql 链接的客户端相当于事务管理器。

分布式事务原理:分段式提交

分布式事务通常采用 2PC 协议,全称 Two Phase Commitment Protocol 。该协议主要为了解决在分布式数据库场景下,所有节点间数据一致性的问题。分布式事务通过 2PC 协议将提交分成两个阶段:

  • prepare ;
  • commit/rollback

阶段一为准备( prepare )阶段 。即所有的参与者准备执行事务并锁住需要的资源。参与者 ready 时,向 transaction manager 报告已准备就绪。
阶段二为提交阶段( commit ) 。当 transaction manager 确认所有参与者都 ready 后,向所有参与者发送 commit 命令。
如下图所示:

事务协调者transaction manager

因为 XA 事务是基于两阶段提交协议的,所以需要有一个事务协调者( transaction 使用 XA 交易時的指導方針和限制 manager )来保证所有的事务参与者都完成了准备工作 ( 第一阶段 ) 。如果事务协调者( transaction manager )收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。 MySQL 在这个 XA 事务中扮演的是参与者的角色,而不是事务协调者( transaction manager )。

Mysql的XA事务分为外部XA和内部XA

  • 外部 XA 用于跨多 MySQL 实例的分布式事务,需要应用层作为协调者,通俗的说就是比如我们在 PHP 中写代码,那么 PHP 书写的逻辑就是协调者。应用层负责决定提交还是回滚,崩溃时的悬挂事务。 MySQL 数据库外部 XA 可以用在分布式数据库代理层,实现对 MySQL 数据库的分布式事务支持,例如开源的代理工具:网易的 DDB ,淘宝的 TDDL 等等。
  • 内部 XA 事务用于同一实例下跨多引擎事务,由 Binlog 作为协调者,比如在一个存储引擎提交时,需要将提交信息写入二进制日志,这就是一个分布式内部 XA 事务,只不过二进制日志的参与者是 MySQL 本身。 Binlog 作为内部 XA 的协调者,在 binlog 中出现的内部 xid ,在 crash recover 时,由 binlog 负责提交。 ( 这是因为, binlog 不进行 prepare ,只进行 commit ,因此在 binlog 中出现的内部 xid ,一定能够保证其在底层各存储引擎中已经完成 prepare) 。

MySQL XA事务基本语法

XA xid [JOIN|RESUME] 启动 xid 事务 (xid 必须是一个唯一值 ; 不支持 [JOIN|RESUME] 子句 )
XA END xid [SUSPEND [FOR MIGRATE]] 结束 xid 事务 ( 使用 XA 交易時的指導方針和限制 不支持 [SUSPEND [FOR MIGRATE]] 子句 )
XA PREPARE xid 准备、预提交 xid 事务
XA COMMIT xid [ONE PHASE] 提交 xid 事务
XA ROLLBACK xid 回滚 xid 事务
XA RECOVER 查看处于 PREPARE 阶段的所有事务

PHP调用MYSQL XA事务示例

1 、首先要确保 mysql 开启 XA 事务支持

SHOW VARIABLES LIKE '%xa%'

如果 innodb_support_xa 的值是 ON 就说明 使用 XA 交易時的指導方針和限制 mysql 已经开启对 XA 事务的支持了。
如果不是就执行:

SET innodb_support_xa = ON

//为XA事务指定一个id,xid 必须是一个唯一值。$xid = uniqid("");

//两个库指定同一个事务id,表明这两个库的操作处于同一事务中$dbtest1->query("XA START '$xid'");//准备事务1$dbtest2->query("XA START '$xid'");//准备事务2

$return = $dbtest1->query("使用 XA 交易時的指導方針和限制 UPDATE member SET name='twm' WHERE ;

throw new Exception("库[email protected]使用 XA 交易時的指導方針和限制 使用 XA 交易時的指導方針和限制 101.17执行update member操作失败!");

$return = $dbtest2->query("UPDATE memberpoints SET point=point+10 WHERE memberid=1") ;

throw new Exception("库[email protected]执行update memberpoints操作失败!");

$dbtest1->query("XA END '$xid'");

$dbtest1->query("XA PREPARE '$xid'");

$dbtest2->query("XA END '$xid'");

$dbtest2->query("XA PREPARE '$xid'");

$dbtest1->query("XA COMMIT '$xid'");

$dbtest2->query("XA COMMIT '$xid'");

catch (Exception $e)

$dbtest1->query("XA ROLLBACK '$xid'");

$dbtest2->query("XA ROLLBACK '$xid'");

XA的性能问题

XA 的性能很低。一个数据库的事务和多个数据库间的 XA 事务性能对比可发现,性能差 10 倍左右。因此要尽量避免 XA 事务,例如可以将数据写入本地,用高性能的消息系统分发数据。或使用数据库复制等技术。只有在这些都无法实现,且性能不是瓶颈时才应该使用 XA 。

无处不在的 MySQL XA 事务

假如协调者意外宕机,很可能整个分布式事务并未完成提交,比如 Tx0 在 DB1 上已提交,而在 DB2 上还处于 PREPARE 的状态。恢复后的协调者需要查看之前的事务日志,得知 Tx0 已经提交了,进而可以提交 DB2 上的悬挂事务,将数据库恢复到一个一致的状态;反之,如果 DB1 上没有 Tx0,DB2 上有一个名为 Tx0 的悬挂事务,协调者通过查看事务日志得知 Tx0 并未提交,因此能够正确地回滚 DB2 上的 Tx0。

XA 协议原本是指基于 2PC 定义的一套交互协议,例如定义了 XA COMMIT、XA PREPARE 这些指令。不过在 MySQL 源码的语境中,很多时候 XA 指的就是 2PC,我们后面会看到,即使在单个 MySQL 进程中,也需要用 2PC 来保证数据一致性。

内部 2PC 事务

举个例子,InnoDB 引擎通过 使用 XA 交易時的指導方針和限制 redo-log 保证自身事务的持久性和原子性,而 X-Engine 引擎通过 WAL(write-ahead log)保证自身事务的持久性和原子性。如果一个事务同时修改了 InnoDB 的表 t1 和 X-Engine 表 t2,问题来了,如果先写入 t1,可能在写 t2 之前发生宕机,于是事务只做一半,违反了原子性。光凭存储引擎自身是无法解决该问题的,不一致发生在不同的存储引擎之间。

更常见的例子发生在 binlog 和 InnoDB 之间。MySQL 的 binlog 可以看作数据的另一个副本,一旦开启 binlog,数据不仅会写入存储引擎,还会写入 binlog 中,并且这两份数据必须严格一致,否则可能出现主备不一致。

对于场景一,我们必须引入一个独立于存储引擎的“外部协调者“来保证 t1 和 t2 上的事务原子性。场景二中也是同理,但是可以稍微巧妙一些——不妨直接让 使用 XA 交易時的指導方針和限制 binlog 来充当事务日志。接下来我们看看具体是如何做的。

MySQL 启动时, init_server_components() 函数按以下规则选择事务协调器(本文代码都取自 MySQL 8.0.21,为了方便阅读会做适当精简。下同):

  1. 如果 binlog 开启,使用 mysql_binlog 事务日志
  2. 否则,如果支持 2PC 的存储引擎多于 1 个,使用 tc_log_mmap 事务日志
  3. 否则,使用 使用 XA 交易時的指導方針和限制 tc_log_dummy 事务日志,它是一个空的实现,实际上就是不记日志

而 TC_LOG 是这三种事务日志具体实现的基类,它定义了事务日志需要实现的接口:

其中 tc_log_mmap 协调器是一个比较标准的事务协调器实现,它会创建一个名为 tc.log 的日志并使用操作系统的内存映射(memory-map,mmap)机制将内容映射到内存中。 tc.log 文件中分为一个一个 PAGE,每个 PAGE 上有多个事务 ID(xid),这些就是由它记录的已经确定提交的事务。

更多的时候,我们用到的都是 mysql_bin_log 这个基于 binlog 实现的事务日志:既然 binlog 反正都是要写的,不妨所有的 Engine 都统一以 binlog 为准,这的确是个很聪明的主意。binlog 中除了 XID 以外还包含许多的信息(比如所有的写入),但对于 TC_LOG 来说只要存在 XID 就足以胜任了。

内部 2PC 事务提交 —— 以 binlog 协调器为例

为了跟踪 MySQL 的事物提交过程,我们执行一条最简单的 UPDATE 语句(autocommit=on),然后看看事务提交是如何进行的。

事务的提交过程入口点位于 ha_commit_trans 函数,事务提交的过程如下:

各个存储引擎会将自己的 prepare、commit 等函数注册到 MySQL Server 层,也就是 handlerton 这个结构体,注册的过程在 ha_innodb.cc 中:

首先是 2PC 的 prepare 阶段, trans_commit_stmt 调用 binlog 协调器的 prepare 接口,但是它什么也不会做,直接去调用存储引擎(以 InnoDB 为例)的 prepare 接口。

2PC 的 commit 阶段, trans_commit_stmt 调用 binlog 协调器的 commit 接口写入 binlog,事务日志被持久化。这一步之后,即使节点宕机,重启恢复时也会将事务恢复至已提交的状态。

最后 binlog 协调器调用存储引擎的 commit 接口,完成事务提交:

以上仅仅是一条更新语句执行的行为,如果是多个事物并发提交,MySQL 会通过 group commit 的方式优化性能,推荐这篇 《图解 MySQL 组提交(group commit)》。

分布式 XA 事务

回到分布式事务上,我们知道 XA 协议本就是为一个分布式事务协议,它规定了 XA PREPARE 、 XA COMMIT 、 XA ROLLBACK 等命令。XA 协议规定了事务管理器(协调者)和资源管理器(数据节点)如何交互,共同完成分布式 2PC 过程。

那么,假如作为 使用 XA 交易時的指導方針和限制 MySQL 的设计者,你会如何实现 XA 协议呢?答案是非常显然的,和内部 2PC 事务复用完全一样的代码就可以了。

为了验证这一点,我们执行一条 XA PREPARE 命令,可以看到果然又来到了 innobase_xa_prepare 。没错,上文中 使用 XA 交易時的指導方針和限制 InnoDB handlerton 中的 prepare 的接口就叫 innobase_xa_prepare ,名字中还带着 xa 的字样。

对于存储引擎来说,外部 XA 还是内部 XA 并没有什么区别,都走的是同一条代码路径。

那为什么之前很多人认为 XA 事务性能差呢?我认为主要有两个原因:

一是分布式本身引入的网络代价,例如事务协调者和存储节点往往不在同一个节点上,这必然会增加少许延迟,并引入更多的 IO 中断代价。

二是因为提交延迟增加导致事务从开始到 commit 之间的持有锁的时间增加了。熟悉并发编程的老手一定知道,加锁并不会让性能下降,锁竞争才是性能的最大敌人。

对于原因一,很大程度上是无可避免的,我们认为这就是“分布式的代价”之一。即便如此,在 PolarDB-X 中,我们也做了许多优化,包括:

  1. 异步提交(async commit):将 2PC 提交从 3 次 RPC 缩减到 1 使用 XA 交易時的指導方針和限制 次 RPC,原理我们会在之后的文章中作详细介绍。
  2. 一阶段提交(1PC):对单分片事务采用 1PC 提交,避免不必要的协调开销,原理和异步提交类似
  3. 合并提交(group commit):以物理节点为单位进行 2PC 提交流程,减少 RPC 代价以及 fsync 代价

对于原因二,其实无论是在单机还是在分布式数据库中,都应该尽可能在业务上避免锁竞争。PolarDB-X 引入了全局 MVCC 事务,其中一个动机便是避免在分布式环境中为读加锁(例如 select for update ),即便不加读锁也可以通过并发转账测试。具体原理可以阅读《PolarDB-X 分布式事务的实现(二):InnoDB CTS 扩展》。

思考:2PC 的本质是什么?

为什么 Lamport 敢断言 Paxos 是唯一正确的共识算法呢?很简单,因为它是以逻辑推导的方式得到的:给定目标和约束,为了达到目的,只能选择此方案。2PC 也是如此。

我们想把 Node 1、2 上已提交的事务撤消,但从 DB 角度说这显然是不可能的(如果从业务上撤消,那也就是 TCC 柔性事务,这已经超出了数据库事务的范畴)。所以我们必须将提交拆成两个部分,并要求第一个部分(即 Prepare 阶段)仍然有“后悔”的机会——既可以继续提交、也可以撤消,即使宕机也不能打破这一点。

就像我们之前说的, XA 协议不过是 2PC 的一个实现标准,几乎就是 1:1 的翻译。批判 2PC 或是 XA 是没有必要也是不应该的,这是唯一正确的分布式提交算法。而 MySQL 的 2PC 实现不仅用于分布式事务所用,它的内部存储引擎也同样依赖 2PC 接口保证事务一致性。

我们常见的分布式数据库基本都采用了 2PC 进行事务提交,区别仅仅在于实现。例如 TiDB 的 Percolator 模型,是 KV 模型上的一种 2PC 实现,本质上是将事务提交日志写到其中一个参与事务的 Key 上;CockrachDB 也类似,不过使用了特殊前缀的 Key 来保存事务日志。

2. 为什么支持 XA?

img

思考:

协议的阻塞机制本身并不是问题,关键问题在于 协议阻塞 遇上 数据锁定。

如果一个参与全局事务的资源 “失联” 了(收不到分支事务结束的命令),那么它锁定的数据,将一直被锁定。进而,甚至可能因此产生死锁。

这是 XA 协议的核心痛点,也是 Seata 引入 XA 模式要重点解决的问题。

基本思路是两个方面:避免 “失联” 和 增加 “自解锁” 机制。(这里涉及非常多技术细节,暂时不展开,在后续 XA 模式演进过程中,会专门拿出来讨论)

  1. 性能差:性能的损耗主要来自两个方面:一方面,事务协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。

思考:

和不使用分布式事务支持的运行场景比较,性能肯定是下降的,这点毫无疑问。

本质上,事务(无论是本地事务还是分布式事务)机制就是拿部分 性能的牺牲 ,换来 编程模型的简单 。

与同为 业务无侵入 的 AT 模式比较:

首先,因为同样运行在 Seata 定义的分布式事务框架下,XA 模式并没有产生更多事务协调的通信开销。

其次,并发事务间,如果数据存在热点,产生锁冲突,这种情况,在 AT 模式(默认使用全局锁)下同样存在的。

所以,在影响性能的两个主要方面,XA 模式并不比 AT 模式有非常明显的劣势。

AT 模式性能优势主要在于:集中管理全局数据锁,锁的释放不需要 RM 参与,释放锁非常快;另外,全局提交的事务,完成阶段 异步化。

3. XA 模式如何实现以及怎样用?

3.1 XA 模式的设计

3.1.1 设计目标

  1. 从 场景 上,满足 全局一致性 的需求。
  2. 使用 XA 交易時的指導方針和限制
  3. 从 应用上,保持与 AT 模式一致的无侵入。
  4. 从 机制 上,适应分布式微服务架构的特点。
  1. 与 AT 模式相同的:以应用程序中 本地事务 的粒度,构建到 XA 模式的 分支事务。
  2. 通过数据源代理,在应用程序本地事务范围外,在框架层面包装 XA 协议的交互机制,把 XA 编程模型 透明化。
  3. 把 XA 的 2PC 拆开,在分支事务 执行阶段 的末尾就进行 XA prepare,把 XA 协议完美融合到 Seata 的事务框架,减少一轮 RPC 交互。

3.1.2 核心设计

1. 整体运行机制

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

  • XA start/XA end/XA prepare + SQL + 注册分支

2. 数据源代理

XA 模式需要 XAConnection。

获取 XAConnection 两种方式:

  • 方式一:要求开发者配置 XADataSource
  • 方式二:根据开发者的普通 DataSource 来创建

第一种方式,给开发者增加了认知负担,需要为 XA 模式专门去学习和使用 XA 数据源,与 透明化 XA 编程模型的设计目标相违背。

第二种方式,对开发者比较友好,和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可。

我们优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection。

类比 AT 模式的数据源代理机制,如下:

img

综合考虑,XA 模式的数据源代理设计需要同时支持第一种方式:基于 XA 数据源进行代理。

类比 AT 模式的数据源代理机制,如下:

img

3. 分支注册

XA start 需要 Xid 参数。

这个 Xid 需要和 Seata 全局事务的 XID 和 BranchId 关联起来,以便由 TC 驱动 XA 分支的提交或回滚。

目前 Seata 的 BranchId 是在分支注册过程,由 TC 统一生成的,所以 XA 模式分支注册的时机需要在 XA start 之前。

把分支注册尽量延后。类似 AT 模式在本地事务提交之前才注册分支,避免分支执行失败情况下,没有意义的分支注册。

这个优化方向需要 BranchId 生成机制的变化来配合。BranchId 不通过分支注册过程生成,而是生成后再带着 BranchId 去注册分支。

这里只通过几个重要的核心设计,说明 XA 模式的基本工作机制。

此外,还有包括 连接保持异常处理 等重要方面,有兴趣可以从项目代码中进一步了解。

3.1.3 演进规划

  1. 第 1 步(已经完成):首个版本(1.2.0),把 XA 模式原型机制跑通。确保只增加,不修改,不给其他模式引入的新问题。
  2. 第 2 步(计划 5 月完成):与 AT 模式必要的融合、重构。
  3. 第 3 步(计划 7 月完成):完善异常处理机制,进行上生产所必需的打磨。
  4. 第 4 步(计划 8 月完成):性能优化。
  5. 第 5 步(计划 2020 年内完成):结合 Seata 项目正在进行的面向云原生的 Transaction Mesh 设计,打造云原生能力。

3.2 XA 模式的使用

从编程模型上,XA 模式与 AT 模式保持完全一致。

可以参考 Seata 官网的样例:seata-xa

样例场景是 Seata 经典的,涉及库存、订单、账户 3 个微服务的商品订购业务。

在样例中,上层编程模型与 AT 模式完全相同。只需要修改数据源代理,即可实现 XA 使用 XA 交易時的指導方針和限制 模式与 AT 模式之间的切换。

Seata 项目最核心的价值在于:构建一个全面解决分布式事务问题的 标准化 平台。

img

XA 模式的加入,补齐了 Seata 在 全局一致性 场景下的缺口,形成 AT、TCC、Saga、XA 四大 事务模式 的版图,基本可以满足所有场景的分布式事务处理诉求。

当然 XA 模式和 Seata 项目本身都还不尽完美,有很多需要改进和完善的地方。非常欢迎大家参与到项目的建设中,共同打造一个标准化的分布式事务平台。

seata XA 简介

头图.png

需要注意的是,「seata 使用 XA 交易時的指導方針和限制 的 xa 模式对传统的三阶段提交做了优化,改成了两阶段提交」:

  • 第一阶段首执行 XA 开启、执行 sql、XA 结束三个步骤,之后直接执行 XA prepare。
  • 使用 XA 交易時的指導方針和限制
  • 第二阶段执行 XA commit/rollback。

mysql 目前是支持 seata xa 模式的两阶段优化的。

「但是这个优化对 oracle 不支持,因为 oracle 实现的是标准的 xa 协议,即 xa end 后,协调节点向事务参与者统一发送 prepare,最后再发送 commit/rollback。这也导致了 seata 的 xa 模式对 oracle 支持不太好。」

seata XA 源码

seata 中的 XA 模式是使用数据源代理来实现的,需要手动配置数据源代理,代码如下:

  • 也可以根据普通 DataSource 来创建 XAConnection,但是这种方式有兼容性问题(比如 oracle),所以 seata 使用了开发者自己配置 XADataSource。
  • seata 提供的 XA 数据源代理,要求代码框架中必须使用 druid 连接池。

1. XA 第一阶段

当 RM 收到 DML 请求后,seata 会使用 ExecuteTemplateXA来执行,执行方法 execute 中有一个地方很关键,就是把 autocommit 属性改为了 false,而 mysql 默认 autocommit 是 true。事务提交之后,还要把 autocommit 改回默认。

下面我们看一下 XA 第一阶段提交的主要代码。

1)开启 XA

上面代码标注[1]处,调用了 ConnectionProxyXA 类的 setAutoCommit 方法,这个方法的源代码中,XA start 主要做了三件事:

  • 向 TC 注册分支事务
  • 调用数据源的 XA 使用 XA 交易時的指導方針和限制 Start
  • 把 xaActive 设置为 true

RM 并没有直接使用 TC 返回的 branchId 作为 xa 数据源的 branchId,而是使用全局事务 id(xid) 和 branchId 重新构建了一个。

2)执行 sql

调用 PreparedStatementProxyXA 的 execute 执行 sql。

3)XA end/prepare

  • 调用数据源的 XA end
  • 调用数据源的 XA prepare
  • 向 TC 报告分支事务状态

到这里我们就可以看到,seata 把 xa 协议的前两个阶段合成了一个阶段。

2. XA commit

5.png

看一下 RmBranchCommitProcessor 类的 process 方法,代码如下:

从调用关系时序图可以看出,上面的 handleBranchCommit 方法最终调用了 AbstractRMHandler 的 handle 方法,最后通过 branchCommit 方法调用了 ResourceManagerXA 类的 finishBranch 方法。 使用 XA 交易時的指導方針和限制
ResourceManagerXA 类是 XA 模式的资源管理器,看下面这个类图,也就是 seata 中资源管理器(RM)的 UML 类图:

6.png

上面的 finishBranch 方法调用了 connectionProxyXA.xaCommit 方法,我们最后看一下 xaCommit 方法:

上面调用了数据源的 commit 方法,提交了 RM 分支事务。

到这里,整个 RM 分支事务就结束了。Rollback 的代码逻辑跟 commit 类似。

最后要说明的是,上面的 xaResource,是 mysql-connector-java.jar 包中的 MysqlXAConnection 类实例,它封装了 mysql 提供的 XA 协议接口。