目录

分布式事务XA协议

前言

分布式事务是为了解决微服务架构(形式都是分布式系统)中不同节点之间的数据一致性问题。这个一致性问题本质上解决的也是传统事务需要解决的问题,即一个请求在多个微服务调用链中,所有服务的数据处理要么全部成功,要么全部回滚。当然分布式事务问题的形式可能与传统事务会有比较大的差异,但是问题本质是一致的,都是要求解决数据的一致性问题。

而分布式事务的实现方式有很多种,最具有代表性的是由Oracle Tuxedo系统提出的 XA分布式事务协议。XA协议包括两阶段提交(2PC)和三阶段提交(3PC)两种实现,在介绍两种实现方式之前先介绍一些分布式事务概念。

  • TCC是应用层的2PC实现,X/A协议实际上也是分两阶段提交的编码实现,只是X/A协议依赖于数据库,需要数据库支持X/A协议,TCC的核心思想是"参与事务的应用程序都应该提供三个http接口,由一个事务协调者进行整体事务的协调"
    • Try接口:预留业务资源;跟普通的操作操作差不多,只是有个status来标识为预生效;
    • Confirm接口:确认执行业务操作;update status改为生效;
    • Cancel接口:取消执行业务操作;如果有报错,那么多个分接口都需要需求,比如把status该为取消等
  • 2PC的实现难点在于事务管理器(TM)的异常处理,如同步阻塞、应答超时、TM宕机恢复
  • 2PC与Paxos:Paxos用于相同数据的分布式高可用存储,2PC更擅长不同数据分布式存储的事务一致性

2PC协议(Prepare,Commit, Rollback)

先介绍分布式事务模型:X/Open DTP(Distributed Transaction Processing) Reference Model。

  • X/Open组织定义的一套分布式事务的标准,定义了规范和API接口,由厂商进行具体的实现
  • X/Open DTP定义了三个组件: AP,TM,RM
  • AP(Application Program):使用分布式事务的应用
  • RM(Resource Manager):资源管理器,这里可以是一个DBMS系统,或者消息服务器管理系统,资源管理器必须实现XA定义的接口
  • TM(Transaction Manager):事务管理器,负责协调和管理事务

通常把TM管理的分布式事务称为全局事务,把RM管理的事务称为本地事务。

两阶段提交,是X/Open DTP的一个实现,主要在事务结束动作前加入了一个Prepare阶段:

  • Prepare阶段:Prepare成功的数据,后续Commit一定会成功。
  • Commit/Rollback阶段:提交或回滚数据

DTP很早就指出资源管理器的类型不单单是数据库,还可以是消息服务。

TCC协议(Try,Confirm,Cancel)

TCC编程: 将业务逻辑拆解为Try、Confirm和Cancel三个阶段,实际上两阶段提交的变种。业务场景如:锁单、下单和取消。

TCC编程需要保证幂等性,即:可以被不断调用接口(直至符合预期),因而需要保证多次调用不会导致异常影响(如重复扣款等)。

2pc比较:数据库 vs 应用

比较容易取得共识的结论:不同业务系统之间使用2PC。

那剩下的问题就简单了, 相同业务系统之间是使用数据访问层2PC还是TCC?一般而言,基于研发成本考虑,会建议:新系统由数据库层来实现统一的分布式事务。

但对热点数据,例如商品(票券等)库存,建议使用TCC方案,因为TCC的主要优势正是可以避免长时间锁定数据库资源进而提高并发性。

2PC与Paxos

有一种广为流传的观点:“2PC到3PC到Paxos到Raft”,即认为:

  • 2PC是Paxos的残次版本
  • 3PC是2PC的改进

上述2个观点都是我所不认同的,更倾向于如下认知:

  • 2PC与Paxos解决的问题不同:2PC是用于解决数据分片后,不同数据之间的分布式事务问题;而Paxos是解决相同数据多副本下的数据一致性问题。例如,UP-2PC的数据存储节点可以使用MGR来管理统一数据分片的高可用
  • 3PC只是2PC的一个实践方法:一方面并没有完整解决事务管理器宕机和资源管理器宕机等异常,反而因为增加了一个处理阶段让问题更加复杂

接下来我们分别来介绍下XA协议两种实现方式的原理。

2PC

第一阶段:请求/表决阶段

http://img.cana.space/picStore/20201116113416.png

既然称为两阶段提交,说明在这个过程中是大致存在两个阶段的处理流程。第一个阶段如上图所示,这个阶段被称之为请求/表决阶段。是个什么意思呢?

就是在分布式事务的发起方在向分布式事务协调者(Coordinator)发送请求时,Coordinator首先会分别向参与者(Partcipant)节点A、参与这节点(Partcipant)节点B分别发送 事务预处理请求,称之为Prepare,有些资料也叫"Vote Request"。

说的直白点就是问一下这些参与节点"这件事你们能不能处理成功了",此时这些参与者节点一般来说就会打开本地数据库事务,然后开始执行数据库本地事务,但在执行完成后并不会立马提交数据库本地事务,而是先向Coordinator报告说:“我这边可以处理了/我这边不能处理”。

如果所有的参与这节点都向协调者做了“Vote Commit”的反馈的话,那么此时流程就会进入第二个阶段了。

第二阶段:提交/执行阶段(正常流程)

http://img.cana.space/picStore/20201116114509.png

如果所有参与者节点都向协调者报告说“我这边可以处理”,那么此时协调者就会向所有参与者节点发送“全局提交确认通知(global_commit)”,即你们都可以进行本地事务提交了,此时参与者节点就会完成自身本地数据库事务的提交,并最终将提交结果回复“ack”消息给Coordinator,然后Coordinator就会向调用方返回分布式事务处理完成的结果。

第二阶段:提交/执行阶段(异常流程)

http://img.cana.space/picStore/20201116115131.png

相反,在第二阶段除了所有的参与者节点都反馈“我这边可以处理了”的情况外,也会有节点反馈说“我这边不能处理”的情况发生,此时参与者节点就会向协调者节点反馈“Vote_Abort”的消息。此时分布式事务协调者节点就会向所有的参与者节点发起事务回滚的消息(“global_rollback”),此时各个参与者节点就会回滚本地事务,释放资源,并且向协调者节点发送“ack”确认消息,协调者节点就会向调用方返回分布式事务处理失败的结果。

以上就是两阶段提交的基本过程了,那么按照这个两阶段提交协议,分布式系统的数据一致性问题就能得到满足吗?

实际上分布式事务是一件非常复杂的事情,两阶段提交只是通过增加了事务协调者(Coordinator)的角色来通过2个阶段的处理流程来解决分布式系统中一个事务需要跨多个服务节点的数据一致性问题。但是从异常情况上考虑,这个流程也并不是那么的无懈可击。

假设如果在第二个阶段中Coordinator在接收到Partcipant的"Vote_Request"后挂掉了或者网络出现了异常,那么此时Partcipant节点就会一直处于本地事务挂起的状态,从而长时间地占用资源。当然这种情况只会出现在极端情况下,然而作为一套健壮的软件系统而言,异常Case的处理才是真正考验方案正确性的地方。

XA-两阶段提交协议中的一些问题

性能问题

从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态。各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。

协调者单点故障问题

事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,从而导致参与者节点始终处于事务无法完成的中间状态。

丢失消息导致的数据不一致问题

在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。

既然两阶段提交有以上问题,那么有没有其他的方案来解决呢?

3PC

三阶段提交又称3PC,其在两阶段提交的基础上增加了 CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。

但是性能问题和不一致问题仍然没有根本解决。下面我们还是一起看下三阶段流程的是什么样的?

第一阶段:CanCommit阶段

http://img.cana.space/picStore/20201116161144.png

这个阶段类似于2PC中的第二个阶段中的Ready阶段,是一种事务询问操作,事务的协调者向所有参与者询问“你们是否可以完成本次事务?”,如果参与者节点认为自身可以完成事务就返回“YES”,否则“NO”。而在实际的场景中参与者节点会对自身逻辑进行事务尝试,其实说白了就是检查下自身状态的健康性,看有没有能力进行事务操作。

第二阶段:PreCommit阶段

http://img.cana.space/picStore/20201116161943.png

在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。此时分布式事务协调者会向所有的参与者节点发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。

如果阶段一中有任何一个参与者节点返回的结果是No响应,或者协调者在等待参与者节点反馈的过程中因挂掉而超时(2PC中只有协调者可以超时,参与者没有超时机制)。整个分布式事务就会中断,协调者就会向所有的参与者发送“abort”请求。

第三阶段:DoCommit阶段

http://img.cana.space/picStore/20201116162430.png

在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态” 转变为 “提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。

看到这里,你是不是会疑惑"3PC相对于2PC而言到底优化了什么地方呢?"

相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢?这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。

另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。

以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),但是3PC依然没有完全解决数据不一致的问题。假如在 DoCommit 过程,参与者A无法接收协调者的通信,那么参与者A会自动提交,但是提交失败了(假设此时参与者A宕机,服务器重启后,服务器会回滚任何未完成的XA事务,即使该事务已经达到了PREPARE状态),其他参与者成功了,此时数据就会不一致。

Mysql数据库分布式事务XA实现

Mysql XA事务简介

XA 事务的基础是两阶段提交协议。需要有一个事务协调者来保证所有的事务参与者都完成了准备工作(第一阶段)。如果协调者收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。Mysql 在这个XA事务中扮演的是参与者的角色,而不是协调者(事务管理器)。

Mysql 的XA事务分为内部XA和外部XA。 外部XA可以参与到外部的分布式事务中,需要应用层介入作为协调者;内部XA事务用于同一实例下跨多引擎事务,由Binlog作为协调者,比如在一个存储引擎提交时,需要将提交信息写入二进制日志,这就是一个分布式内部XA事务,只不过二进制日志的参与者是MySQL本身。 Mysql 在XA事务中扮演的是一个参与者的角色,而不是协调者。

Mysql XA事务基本语法

  • XA {START|BEGIN} xid [JOIN|RESUME] 启动一个XA事务 (xid 必须是一个唯一值; [JOIN|RESUME] 字句不被支持)
  • XA END xid [SUSPEND [FOR MIGRATE]] 结束一个XA事务 ( [SUSPEND [FOR MIGRATE]] 字句不被支持)
  • XA PREPARE xid 准备
  • XA COMMIT xid [ONE PHASE] 提交XA事务
  • XA ROLLBACK xid 回滚XA事务
  • XA RECOVER 查看处于PREPARE 阶段的所有XA事务

事务标识符xid

xid 是一个事务标识符,它由客户端提供或者有mysql服务器生成。

xid的格式一般为 xid : gtrid [, bqual [, formatID]] ;gtrid是一个全局事务标识符,bqual是一个分支限定符,formatID是一个数字,用于标识由gtrid和bqual值使用的格式。

XA事务状态进展过程

  1. 使用XA START 启动一个XA事务,并把它置为ACTIVE状态。

  2. 对一个ACTIVE XA事务,发布构成事务的SQL语句,然后发布一个XA END 语句,XA END 把事务置为IDLE状态。

  3. 对一个IDLE XA 事务, 发布一个XA PREPARE语句或者一个XA COMMIT … ONE PHASE语句: 前者把事务置为PREPARE状态,此时XA RECOVER 语句的输出包含事务的xid值(XA RECOVER 语句会列出所有处于PREPARE状态的XA事务); 后者用于预备和提交事务,不会被XA RECOVER列出,因为事务已经终止。

  4. 对一个PREPARE XA 事务,可以发布一个XA COMMIT语句来提交和终止事务,或者发布一个XA ROLLBACK 来回滚并终止事务。

简单的XA事务操作流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
mysql> XA START 'xatest';
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO test (name,tel) VALUES ('123','123');
Query OK, 1 row affected (0.00 sec)

mysql> XA END 'xatest';
Query OK, 0 rows affected (0.00 sec)

mysql> XA PREPARE 'xatest';
Query OK, 0 rows affected (0.00 sec)

mysql> 
mysql> 
mysql> XA COMMIT 'xatest';
Query OK, 0 rows affected (0.00 sec)

XA RECOVER 介绍

这里指当与MySQL的交互出现异常,例如与MySQL的会话断开、MySQL宕机等,如何获取并恢复处于Prepare状态下的2PC事务,并进行提交或回滚处理。XA RECOVER 可以列出所有处于PREPARE状态的XA事务:

1
2
3
4
5
6
7
mysql> XA RECOVER;
+----------+--------------+--------------+--------+
| formatID | gtrid_length | bqual_length | data   |
+----------+--------------+--------------+--------+
|        1 |            6 |            0 | xa1000 |
+----------+--------------+--------------+--------+
1 row in set (0.00 sec)

注释:

  1. formatID 是事务xid的formatID部分。

  2. gtrid_length 是xid的gtrid部分的长度,以字节为单位。

  3. bqual_length 是xid的bqual部分的长度,以字节为单位。

  4. data 是xid的gtrid部分和bqual部分的串联。

注意事项

在用一个客户端环境下,XA事务和本地(非XA)事务互相排斥,如果已经发布了XA START来开启一个事务,则本地事务不会被启动,直到XA事务被提交或者被回滚为止;相反的,如果已经使用START TRANSACTION启动一个本地事务,则XA语句不能被使用,直到该事务被提交或者回滚为止,而且XA事务仅仅被InnoDB存储引擎支持。

如果XA事务达到PREPARE状态时MySQL服务器宕机,当服务器重启后,服务器会回滚任何未完成的XA事务,即使该事务已经达到了PREPARE状态;如果客户端连接终止,而服务器继续运行,服务器将回滚任何未完成的XA事务,即使该事务已经达到PREPARED状态。

参考