分布式事务实战---XA两阶段提交(2PC)方案详解

2022-11-30 15:39:53 浏览数 (1)

XA,2PC,two-phase commit protocol,两阶段事务提交采⽤的是 X/OPEN 组织定义的DTP 模型所抽象的:

  • AP 应用程序,Application Program,定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作
  • TM(事务管理器) Transaction Manager,负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等
  • RM(资源管理器) Resource Manager,如数据库、文件系统等,并提供访问资源的方式
XA 接口

xa_start :负责开启或者恢复一个事务分支 xa_end: 负责取消当前线程与事务分支的关联 xa_prepare:询问 RM 是否准备好提交事务分支 xa_commit:通知 RM 提交事务分支 xa_rollback: 通知 RM 回滚事务分支 xa_recover : 需要恢复的 XA 事务

保证分布式事务的强⼀致性。其中 TM 与 RM 间采⽤ XA 协议进⾏双向通信。 XA 整体设计思路可概括为:在现有事务模型基础上微调扩展而实现的分布式事务。

与传统的本地事务相⽐,XA 事务增加了准备阶段,数据库除了被动接受提交指令外,还可以反向通知调⽤⽅事务是否可以被提交。TM 可以收集所有分⽀事务的准备结果,并于最后进⾏原⼦提交,以保证事务的强⼀致性

  • 两阶段提交模型

Java 通过定义 JTA 接口实现了 XA 模型

JTA 接口中的 ResourceManager 需要数据库⼚商提供 XA 驱动实现,TransactionManager 则需要事务管理器的⼚商实现,传统的事务管理器需要同应⽤服务器绑定,因此使⽤的成本很⾼。而嵌⼊式的事务管器可以以 jar 包的形式提供服务,同 Apache ShardingSphere集成后,可保证分⽚后跨库事务强⼀致性。通常,只有使⽤了事务管理器⼚商所提供的 XA 事务连接池,才能⽀持 XA 的事务。Apache ShardingSphere在整合 XA 事务时,采⽤分离 XA 事务管理和连接池管理的⽅式,做到对应⽤程序的零侵⼊。

MySQL 从5.0.3开始支持 InnoDB 引擎的 XA 分布式事务,MySQL Connector/J 从5.0.0版本开始支持 XA。

在 DTP 模型中,MySQL 属于资源管理器(RM)。分布式事务中存在多个 RM,由事务管理器 TM 来统一进行协调。

MySQL 的 XA
  • XA {START | BEGIN} xid [JOIN | RESUME] 开启XA事务,如果使用的是XA START而非XA BEGIN,那么不支持[JOIN | RESUME],xid是个唯一值,表示事务分支标 全局 分支 id
  • XA END xid [SUSPEND [FOR MIGRATE]] 结束一个xA事务,不支持[SUSPEND [FOR MIGRATE]]
  • XA PREPARE xid

准备提交

  • XA COMIT xid [ONE PHASE] 提交,如果使用了 ONE PHASE,贼表示使用一阶段提交。两阶段提交协议中,如果只有一个 RM 参与,那么可以优化为一阶段提交
  • XA ROLLBACK xid

回滚

  • XA recover[convert xid]

列出所有处于 prepare 阶段的 XA 事务

代码语言:javascript复制
mysql> select * from t;
 ---- ------ ------ 
| id | c    | d    |
 ---- ------ ------ 
|  0 |    0 |    0 |
|  5 |    5 |    5 |
| 10 |   10 |   10 |
| 15 |   15 |   15 |
| 20 |   20 |   20 |
| 25 |   25 |   25 |
| 30 |   10 |   30 |
 ---- ------ ------ 
7 rows in set (0.01 sec)

mysql> xa start 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t values(1,2,3);
Query OK, 1 row affected (0.01 sec)

mysql> update t set id = 11 where id = 10;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> xa end 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> xa prepare 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> xa commit 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t;
 ---- ------ ------ 
| id | c    | d    |
 ---- ------ ------ 
|  0 |    0 |    0 |
|  1 |    2 |    3 | # 添加的记录
|  5 |    5 |    5 |
| 11 |   10 |   10 | # 修改的记录
| 15 |   15 |   15 |
| 20 |   20 |   20 |
| 25 |   25 |   25 |
| 30 |   10 |   30 |
 ---- ------ ------ 
8 rows in set (0.01 sec)

mysql> xa start 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t values(2,5,8);
Query OK, 1 row affected (0.00 sec)

mysql> xa end 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> xa prepare 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> xa recover;
# gtr 全局事务 id
# bq 分支事务 id,这就能区分是大事务,还是小事务
 ---------- -------------- -------------- ------ 
| formatID | gtrid_length | bqual_length | data |
 ---------- -------------- -------------- ------ 
|        1 |            3 |            0 | x01  |
 ---------- -------------- -------------- ------ 
1 row in set (0.00 sec)

mysql> xa start 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t values(2,5,8);
Query OK, 1 row affected (0.00 sec)

mysql> xa end 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> xa prepare 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> xa recover;
 ---------- -------------- -------------- ------ 
| formatID | gtrid_length | bqual_length | data |
 ---------- -------------- -------------- ------ 
|        1 |            3 |            0 | x01  |
 ---------- -------------- -------------- ------ 
1 row in set (0.00 sec)

mysql> xa rollback 'x01';
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t;
 ---- ------ ------ 
| id | c    | d    |
 ---- ------ ------ 
|  0 |    0 |    0 |
|  1 |    2 |    3 |
|  5 |    5 |    5 |
| 11 |   10 |   10 |
| 15 |   15 |   15 |
| 20 |   20 |   20 |
| 25 |   25 |   25 |
| 30 |   10 |   30 |
 ---- ------ ------ 
8 rows in set (0.00 sec)

实践分支事务 id

代码语言:javascript复制
mysql> xa start 'x01', 'b01';
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t;
 ---- ------ ------ 
| id | c    | d    |
 ---- ------ ------ 
|  0 |    0 |    0 |
|  1 |    2 |    3 |
|  5 |    5 |    5 |
| 11 |   10 |   10 |
| 15 |   15 |   15 |
| 20 |   20 |   20 |
| 25 |   25 |   25 |
| 30 |   10 |   30 |
 ---- ------ ------ 
8 rows in set (0.00 sec)

mysql> insert into t values(2,5,8);
Query OK, 1 row affected (0.00 sec)

mysql> select * from t;
 ---- ------ ------ 
| id | c    | d    |
 ---- ------ ------ 
|  0 |    0 |    0 |
|  1 |    2 |    3 |
|  2 |    5 |    8 |
|  5 |    5 |    5 |
| 11 |   10 |   10 |
| 15 |   15 |   15 |
| 20 |   20 |   20 |
| 25 |   25 |   25 |
| 30 |   10 |   30 |
 ---- ------ ------ 
9 rows in set (0.00 sec)

mysql> xa end 'x01', 'b01';
Query OK, 0 rows affected (0.00 sec)

mysql> xa prepare 'x01', 'b01';
Query OK, 0 rows affected (0.01 sec)

mysql> xa recover;
 ---------- -------------- -------------- -------- 
| formatID | gtrid_length  | bqual_length | data   |
 ---------- -------------- -------------- -------- 
|        1 |            3 |            3 | x01b01 |
 ---------- -------------- -------------- -------- 
1 row in set (0.01 sec)
# gtrid_length 全局事务 id 的长度
# bqual_length 分支事务 id 的长度

mysql> xa commit 'x01', 'b01';
Query OK, 0 rows affected (0.00 sec)

mysql> xa recover;
Empty set (0.00 sec)

mysql> select * from t;
 ---- ------ ------ 
| id | c    | d    |
 ---- ------ ------ 
|  0 |    0 |    0 |
|  1 |    2 |    3 |
|  2 |    5 |    8 |
|  5 |    5 |    5 |
| 11 |   10 |   10 |
| 15 |   15 |   15 |
| 20 |   20 |   20 |
| 25 |   25 |   25 |
| 30 |   10 |   30 |
 ---- ------ ------ 
9 rows in set (0.00 sec)
  • MySQL XA事务状态 ACTIVE状态的XA事务,我们可以执行构成事务的SQL语句,然后发布一个XA END语句。XA END把事务放入IDLE状态。

XA事务和非XA事务(即本地事务)是互斥的。 例如,已经执行了 XA START 开启一个XA事务,则本地事务不会被启动,直到XA事务已被提交或被回滚为止。相反的,若已使用START TRANSACTION启动一个本地事务,则XA语句不能被使用,直到该事务被提交或被回滚为止。

时序图
  • 完整的 XA 事务处理时序图
  • 单个 MySQL 的内部操作
XA 过程中,事务失败怎么办?

业务 SQL 执行过程,某个 RM 崩溃怎么处理,全部 prepare 后,某个 RM 崩溃怎么处理,commit 时,某个 RM 崩溃怎么办?

MySQL < 5.7的bug
  • 已经prepare的事务, 在客户端退出或者服务宕机的时候,2PC的事务 会被回滚。
  • 在服务器故障重启提交后, 相应的Binlog被丢失

MySQL 5.6版本在客户端退出的时候,自动把已经prepare的事务回滚了 ,那么MySQL为什么要这样做?这主要取决于MySQL的内部实现,MySQL 5.7以前的版本,对于prepare的事务,MySQL 是不会记录binlog的(官方说是减少fsync, 起到了优化的作用)。只有当分布式事务提交的时候才会把前面的操作写入binlog信息,所以对于binlog来说,分布式事务与普通的事务没有区别,而prepare以 前的操作信息都保存在连接的I0 CACHE中,如果这个时候客户端退出了,以前的binlog信息都会被丢失,再次重连后允许提交的话, 会造成Binlog丢失,从而造成主从数据的不一致,所以官方在客户端退出的时候直接把已经prepare的事务都回滚了!

MySQL > 5.7版本的优化

https://dev.mysql.com/worklog/task/?id=6860,MySQL对于分布式事务,在prepare的时候就完成了写Binlog的操作,通过新增一种叫XA _prepare_ log_ event的event类型来实现,这与以前版本的主要区别(以前版本prepare时不写Binlog)。

事务管理器,负责协调多个数据库(资源管理器)的事务。

  1. 事务管理器先问各个DB:预提交 ok 吗?
  2. 如果每个数据库都回复ok,即预提交成功,开始正式提交事务,在各DB开始执行操作,这里失败会有失败异常重试,日志分析,人工重试。

X/Open,即现在的open group,是一个独立的组织,主要负责制定各种行 业技术标准。X/Open组织主要由各大知名公司或厂商支持,这些组织不光遵循X/Open组织定义的行业技术标准,也参与到标准的制定。

故障分析

单点故障

协调者出错,事务会失败。

集群部署即可。

阻塞资源

占用数据库连接,性能低。

保持连接是为了第一阶段和第二阶段使用的是同一个事务,确保提交或回滚事务,只需将操作前、后的数据落库记录,就能将当前的连接放弃了。 将连接释放掉,就能保证系统的性能了。

提交阶段什么都不用做,因为数据库已有操作后记录。 回滚也可以,因为已经记录过操作前的记录。

数据不一致

二阶段出错,数据不一致

适用场景

适合单块应用中,跨多库的分布式事务,而且因其严重依赖DB层面解决事务,所以效率很低,不适合高并发场景。

互联网公司基本都不用,因某个系统内部如果出现跨多库的操作,是不合规的。现在的微服务,一个大的系统分成几十甚至上百个服务。一般规约每个服务只能操作自己对应的一个数据库。

如果你要操作别的服务对应的库,不允许直连别的服务的库。要操作别人的服务的库,必须通过调用别的服务的接口

主流的开源XA分布式事务解决方案

Element

ATOMIKOS

narayanna

seata

TM

去中心化设计,性能较高

去中心化设计,性能较高

中心化设计,性能较差,bug 多

日志存储件

只支持文件

文件、数据库

文件、数据库

扩展性

较好

一般

一般

事务恢复

只支持单机事务恢复

集群模式恢复

问题很多,未能正确恢复

XA

标准 XA 实现

标准 XA 实现

非标准的 XA 实现

0 人点赞