MySQL XA事务

2PC事务

2pc事务即通常所说的两阶段提交事务,全称Two Phase Commitment Protocol,主要为了解决在分布式部署的情况下,所有节点间数据一致性问题。在分布式部署的情况下,事务的提交会变得相对比较复杂,因为多个节点的存在,可能存在部分节点提交失败的情况,这个时候需要保证数据一致性的话,需要回滚那些能够提交的节点上的事务,总而言之,在分布式提交时,只要发生一个节点提交失败,则所有的节点都不能提交,只有当所有节点都能提交时,整个2pc事务才允许被提交。那么,怎样来保证所有的分布式节点都可以提交?其实,2pc事务的提交被分成两个阶段:第一阶段:prepare,第二阶段:commit/rollback。第一阶段的prepare只是用来询问每个节点事务是否能提交,只有当得到所有节点的“许可”的情况下,第二阶段的commit才能进行,否则就rollback。

MySQL实现缺陷

一直以来,MySQL官方对2pc事务的支持都不是很好,主要表现在已经prepare的事务,在客户端退出或者服务宕机的时候,2pc的事务会被回滚,或者在服务器重启提交后,相应的binlog被丢失,这个问题在MySQL官方大多数版本中都一直存在,直到MySQL-5.7.7版本,官方才修复了该问题,下面我们会详细介绍下该问题的具体表现和官方修复方法,我们这里分别采用官方MySQL-5.6.27版本(未修复)和MySQL-5.7.9版本(已修复)进行验证。
先来看下存在的问题,我们先创建一个表如下:
create table t(id int auto_increment primary key, a int)engine=innodb;
对于上述表,通过如下操作进行数据插入:
通过上面的操作,我们创建了一个2pc事务,并且prepare没有返回错误,说明该2pc事务可以被提交,通过xa recover查看显示如下:
这个时候我们退出客户端后重连,通过xa recover发现刚才创建的2pc事务不见了,这是为什么呢?
这个问题主要的原因在于:MySQL在客户端退出的时候,自动把已经prepare的事务回滚了,那么MySQL为什么要这样做?这主要取决于MySQL的内部实现,MySQL-5.7以前的版本,对于prepare的事务,MySQL是不会记录binlog的(官方说是优化),只有当事务提交的时候才会把前面的操作写入binlog信息,所以对于binlog来说,2pc的事务与普通的事务没有区别,而prepare以前的操作信息都保存在连接的IO_CACHE中,如果这个时候客户端退出了,以前的binlog信息都会被丢失,再次重连后允许提交的话,会造成binlog丢失,从而造成主从数据的不一致,所以官方在客户端退出的时候直接把已经prepare的事务都回滚了!
官方的做法,貌似干得很漂亮,牺牲了一点标准化的东西,至少保证了主从数据的一致性,其实不然,如果已经prepare后在客户端退出之前,MySQL发生了宕机,这个时候又会怎样?
MySQL在某个2pc事务prepare以后宕机,宕机前操作该事务的连接并没有断开,这个时候已经prepare的事务并不会被回滚,所以在MySQL重新启动后,引擎层通过recover机制能恢复改事务,当然该事务的binlog已经在宕机过程中被丢失,这个时候,如果去提交,则会造成主从数据的不一致(提交没有记录binlog,从上丢失该条数据),所以对于这种情况,官方一般建议直接回滚已经prepare的事务。

MySQL 5.7改进

以上是MySQL-5.7以前版本MySQL在2pc事务上的各种问题,那么5.7版本官方做了哪些改进?这个可以从官方的WL#6860(http://dev.mysql.com/worklog/task/?id=6860)描述上得到一些信息,我们还是本着没有实践就没有发言权的态度,从具体的操作上来分析下MySQL-5.7的改进方法。
还是以上面同样的表结构进行同样的操作如下:
 这个时候,我们通过mysqlbinlog来查看下主上的binlog,结果如下:
同时也对比下从上的relay log,如下:
通过上面的操作,我们明显发现在prepare以后,从xa start到xa prepare之间的操作都被记录到了master的binlog中,然后通过复制关系传到了slave上。也就是说MySQL-5.7开始,MySQL对于2pc的事务,在prepare的时候就完成了写binlog的操作,通过新增一种叫XA_prepare_log_event的event类型来实现,这是与以前版本的主要区别(以前版本prepare时不写binlog)
当然仅靠这一点是不够的,因为我们知道slave通过sql thread来回放relay log信息,由于prepare的事务能阻塞整个session,而回放的sql thread只有一个(不考虑并行回放),那么sql thread会不会因为被2pc事务的prepare阶段所阻塞,从而造成整个sql thread回放出现问题?这也正是官方要解决的第二个问题:怎么样能使sql thread在回放到2pc事务的prepare阶段时,不阻塞后面event的回放?其实这个实现也很简单(在xa.cc::applier_reset_xa_trans),只要在sql thread回放到prepare的时候,进行类似于客户端断开连接的处理即可(把相关cache与sql thread的连接句柄脱离),最后在slave上,我们通过xa recover可以查到如下信息:
至于上面的事务什么时候提交,一般等到master上进行xa commit ‘mysql57’后,slave上也同时会被提交

存在的问题

官方在5.7修复了xa prepare不记录binlog的问题,同时也引入了一些问题,目前发现与之相关的bug有2个:

  • 删除表后提交问题
  • partial Transaction造成slave SQL thread中断

对于第一个问题,假如我们有如下两个事务:

session1:

xa start ‘a’;

insert into t values(1);

xa end ‘a’;

xa prepare ‘a’;

exit;

session2:

drop table t;

xa recover;

xa commit ‘a’;

由于session1在xa事务prepare后退出,session2可以通过xa recover查到当前悬挂的xa事务,但是session2在xa recover之前已经通过drop命令删除了t表,在xa commit时,t表已经不存在,但是当前测试中,xa commit并不会报错。

至于partial Transaction问题,由于slave上relay log的写入并不是事务原子的,而且relay log文件的大小固定,所以可能会出现部分事务写入后达到relay log大小限制,进行relay log文件切换的情况,MySQL一般为了防止这种情况,会在每个relay log文件rotate的事务强行插入一个rollback,这样可以使partial write的事务被回滚,但是当存在xa事务的时候,relay log中的记录可能是这样的:

xa start ‘a’;

insert into …

xa end ‘b’;

rollback

上述relay log在被SQL线程执行的时候会报错,导致复制被中断。

发表评论

电子邮件地址不会被公开。 必填项已用*标注