1 数据库事务

1.1 一般本地事务

分布式事务也是事务,事务的 ACID 根本个性仍旧必须合乎:

A:Atomic,原子性,事务内所有 SQL 作为原子工作单元执行,要么全副胜利,要么全副失败;

C:Consistent,一致性,事务实现后,所有数据的状态都是统一的。如事务内A给B转100,只有A减去了100,B账户则必然加上了100;

I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的批改必须与其余事务隔离;

D:Duration,持久性,即事务实现后,对数据库数据的批改被长久化存储。

一般的非分布式事务,在一个过程外部,基于锁依赖于快照读和以后读,比拟好实现 ACID 来保障事务的可靠性。但分布式事务参与方通常在不同机器的不同实例上,原来的部分事务的锁不能保障分布式事务的ACID个性,须要引入新的事务框架,MySQL的分布式事务是基于2PC(二阶段提交)实现,上面具体介绍下2pc分布式事务。

1.2 基于2pc的分布式事务

分布式事务有多种实现形式,如2PC(二阶段提交)、3PC(三阶段提交)、TCC(弥补事务)等,MySQL是基于 2PC 实现的分布式事务,上面介绍 2PC 分布式事务实现形式。

两阶段提交:Two-Phase Commit , 简称2PC,为了使基于分布式系统架构下的所有节点在进行事务提交时放弃一致性而设计的一种算法。

2PC的算法思路能够概括为,参与者将操作成败告诉协调者,再由协调者依据所有参与者的反馈情报,决定各参与者是否要提交操作还是停止操作。这里的参与者能够了解为 Resource Manager (RM),协调者能够了解为 Transaction Manager(TM)。

下图阐明了RM和TM在分布式事务中的运作过程:

第一阶段提交:TM 会发送 Prepare 到所有RM询问是否能够提交操作,RM 接管到申请,实现本身事务提交前的筹备工作并返回后果。

第二阶段提交:依据RM返回的后果,所有RM都返回能够提交,则 TM 给 RM 发送 commit 的命令,每个 RM 实现本人的提交,同时开释锁和资源,而后 RM 反馈提交胜利,TM 实现整个分布式事务;如果任何一个 RM 返回不能提交,则波及分布式事务的所有 RM 都须要回滚。

2 MySQL 分布式事务XA

MySQL分布式事务XA是基于下面的2pc框架实现,上面具体介绍MySQL XA相干内容。

2.1 XA事务规范

X/Open 这个组织定义的一套分布式XA事务的规范,定义了标准和API接口,而后由厂商进行具体的实现。

XA标准中分布式事务由AP,RM,TM组成:

如上图,应用程序AP定义事务边界(定义事务开始和完结),并拜访事务边界内的资源。资源管理器RM治理共享的资源,也就是数据库实例。事务管理器TM负责管理全局事务,调配事务惟一标识,监控事务的执行进度,并负责事务的提交、回滚、失败复原等。MySQL实现了XA规范语法,提供了下面的RMs能力,能够让下层利用基于它疾速反对分布式事务。

2.2 MySQL XA语法

XA START xid:开启一个分布式事务xid。

XA END xid: 将分布式事务xid置于 IDLE 状态,示意事务内的SQL操作实现。

XA PREPARE xid: 事务xid本地提交,胜利状态置于 PREPARED 失败则回滚。

XA COMMIT xid: 事务最终提交,实现长久化。

XA ROLLBACK xid: 事务回滚终止。

XA RECOVER: 查看 MySQL 中存在的 PREPARED 状态的 XA 事务。

(1)语法要点

参加分布式事务的实例之间,在数据库内核视角没有间接关联,相互不感知状态,且一个分布式事务中各个节点上的子事务均可独自执行无依赖,他们之间的关联是通过全局事务号在应用层建设的。

与一般事务比,XA事务开启时多了一个全局事务号,完结时多了一个end动作 和 prepare动作。

XA START, 开启一个分布式事务,须要指定分布式事务号。
XA END ,在外部仅是一个状态变动,申明以后XA事务完结,不容许追加新的sql语句,无其它作用,业界有人提出XA事务框架去掉这一步,缩小一次网络交互,进步性能。

XA PREPARE,写 binlog 和 redo log,预提交事务,并将分布式事务信息保留到全局内存构造,让其它连贯能够查问、回滚、提交,如果 prepare 失败则回滚。

XA COMMIT,真正提交事务,批改事务状态,开释锁资源。如果实例上 XA PREPARE 曾经胜利,那么它的 XA COMMIT 肯定能胜利。

XA事务示例:201用户给202用户转账1000元,简化如下:

第1步,开启一个分布式事务,xa_ts:10001是应用层定义的全局事务号,实例1和实例2通过它来构建分布式事务。

第2、3步是一般事务语句。

第4步,声名xa事务完结,在此之后不能再追加更新插入查问等语句,不属于这个分布式事务也不容许,其它语句放在xa commit或xa rollback之后。

第5步,prepare 胜利后,下层利用能够发动第6步提交事务。留神,必须是所有参加这个分布式事务的全副节点均 prepare 胜利,即实例1和实例2都实现prepare,利用端能力发动提交,两阶段提交的框架外围点就在此。

如果有节点在前5步不能胜利,所有参加分布式事务的节点都必须回滚。如实例2是账户加1000元,基本上什么状况都能胜利,必定能胜利执行第5步,但实例1就未必了,账户要扣1000元,可能资金不够,会出错回滚,若实例1不能执行到prepare,所有分布式事务参与者也必须回滚,所以实例2也要回滚。如果第5步全副胜利,有一个节点执行了第6步提交了事务,那么所有节点必须要均提交,否则就会导致数据不统一。处于xa prepare不提交会占用资源,残留xa事务等价于存在长事务,对刷脏和purge等都有影响,业务层最好要立刻提交。

(2)残留XA事务如何解决

下面说到xa事务不提交等价于长事务,一旦prepare胜利要立刻提交,否则会带来很多问题。然而数据库crash或利用零碎出错crash等起因都可能导致xa事务未能全副提交,这些残存XA事务如何解决?这就要用到下面的 XA RECOVER语法了,执行xa recover 查看未提交XA事务,抉择对应的进行rollback或commit。如果仅 gtrid_length字段有值个别能够间接 xa rollback/commit xid形式回滚或提交,xid就是xa recover中data。

如果gtrid_length和bqual_length 都有值,回滚或提交则绝对简单一些,须要以上面形式提交或回滚:

xa rollback/commit gtrid, bqual,formatid ;

gtrid 和 bqual被拼接在 data字段中,须要按他们长度切分,以上面未提交xa事务里第一个为例,gtrid_length 为34,示意data中前34个字符为gtrid, bqual_length 为22,示意data中后22个字符为bqual,那么对对其回滚或提交形式可示意如下:

xa rollback '10.177.197.41.tm163721155374124700', '10.177.197.41.tm881366', 1096044365;
xa commit '10.177.197.41.tm163721155374124700','10.177.197.41.tm881366',1096044365;

如果data中有其它特殊字符,也能够转成16进制整数形式解决,执行语句如下:

XA recover convert xid;----转换16进制显示

因为是16进制数,字符做了转换,data中字符数会翻倍,回滚或提交内容要同步调整,将data中字符也要翻倍再拆分,如上grtrid长度34,则data中前34*2个16进制数字是gtrid,bqual长度22,则后44个16进制数字是bqual,回滚或提交语法如下:

xa rollback 0x31302E3137372E3139372E34312E746D313633373231313535323835323234363035, 0x31302E3137372E3139372E34312E746D383831323038,1096044365;
xa commit,
0x31302E3137372E3139372E34312E746D313633373231313535323835323234363035, 0x31302E3137372E3139372E34312E746D383831323038,1096044365;

留神:下面的提交或回滚都可能报xid不存在,这不肯定是xid写错了,也可能是开启这个XA事务的连贯并未断开,其它连贯不能解决这个XA事务,这里是MySQL报错不精确。

(3)提交还是回滚的根据

下面给出如何进行提交或回滚的办法,然而提交or回滚应该抉择哪个?

残留XA事务是提交还是回滚,必须要由业务决定,谁开启XA事务,构建了散布事务管理器TM,谁就必须为这个事务负责到底。

单个数据库视角无奈判断出这个XA事务是应该提交还是应该回滚,不论选哪种都可能会导致全局数据出错,运维同学在解决时肯定要与业务方确定好该事务是提交还是回滚,取得受权后再操作。以下面转账为例,201用户给202转1000元,都prepare胜利,发动commit,此时202用户实例产生故障重启,未实现commit,重启之后有残留XA事务,此时若201提交胜利,那么202必须提交,如果201未胜利,202能够先201一起提交或一起回滚,由应用层事务管理器TM来决定。如果201提交胜利,202回滚则201扣了1000,202未收到,对账则钱少了。如201回滚了,202提交,则202加了1000,201未扣,对账则钱多了。

2.3 MySQL XA事务设计上的“坑”

(1)设计上的缺点

基于binlog的主从复制是MySQL高可用的基石,这也是MySQL能宽泛风行应用的最重要因素。在MySQL外部,对于一般事务(非XA事务),innodb等引擎和binlog为了保持数据的一致性,就是用的 2PC ,为了辨别于XA事务的2PC ,称之为外部两阶段提交。外部2pc应用binlog是作为协调者(TM),外部prepare时先写redo再写binlog,都长久化(受刷盘参数策略影响)后再提交。当产生Crash重启时,会先复原出所有prepare胜利的事务,把外面的xid事务号取出来,再到协调者Binlog中去找,如果binlog中有这个xid则阐明innodb和binlog都执行胜利,等价于内部xa 事务两个参加节点都prepare胜利,则持续提交,如果binlog中找不到,刚阐明只在引擎层实现,须要回滚,如果某个进行的事务xid在prepare中未找到,则阐明prepare未实现,间接回滚,这个程序肯定是先写Redo log,最初写Binlog。

那么处于XA prepare 状态的分布式事务到底是一个什么样的状态?分布式XA事务也是基于一般事务实现,实际上就是一个反对挂起,反对让其它会话持续提交或回滚,反对crash或重启之后还能复原这种挂起状态的一般事务。

一般事务的prepare动作是产生在显式commit之后,先写redo后再写binlog。XA事务的prepare产生在显式XA commit之前,它须要生成binlog,而后再写redo,这与一般事务是相同的,这就导致这个内部2pc事务的外部2pc提交短少了一个协调者,某些状况下会导致数据库不统一。

一个XA事务的binlog由两局部组成,从xa start到xa prepare是一个不可分原子语句块,xa commit又是一个原子语句块,且别离有各自的gtid,如下图binlog:

事务号为 X'7831',X'',1 的分布式事务prepare之后,两头插入了很多一般事务,而后再执行的xa commit。

一个XA事务的binlog被切分成了两个独立的局部,如果在主节点在生成XA prepare binlog之后产生crash, 还没有在引擎层做prepare,重启之后引擎层中因没有实现prepare动作而回滚。但在主从架构中,只有binlog失常产生就可能会同步到Slave机,这种状况下会导致slave机上多了这个xa prepare的两头状事务,最终复制呈现问题。这个问题曾经被发现多年,官网确认了bug,始终未修复(https://bugs.mysql.com/bug.ph...)。

(2)遇到该问题解决思路

尽管咱们要尽量避免呈现故障,但也做好面对任何故障的筹备,谋而后动,有招不乱!

在惯例连贯中,MySQL的XA事务执行prepare之后,通常不能执行其它非xa语句,会报错揭示以后正在xa事务中。但在复制的sql 回放线程中,执行完xa prepare之后,能够间接执行其它非此xa事务的sql,因为在master端生成的XA事务Binlog可能就是离开的,如上图例子就是。所以slave机sql线程执行完xa prepare的binlog后,是被容许接着失常执行其它事务的binlog的。如果xa preapre过程master上产生crash,刚好生成了binlog,但没有做完后续的prepare动作,备机收到了这个xa preare动作的binlog,master重启后会回滚掉这个事务,不会再生成这个xa事务后续binlog,这会导致备机执行完xa prepare后始终挂起,占用的锁等资源不会开释,直到新同步过去的binlog与之抵触报错,才会裸露问题。

要修复分两种状况解决:

状况1:基于gtid的复制,应该间接会报gtid反复谬误(揣测,本地没能复现)。master上重启应该会回滚掉了前半个XA事务,前面事务会从新生成这个雷同gtid的事务,导致复制出错,此时进行复制,将备机上这半个XA事务回滚,并reset gtid到之前的gtid,重建复制即可。留神这里可能有多个XA事务在Binlog中处于prepare状态,须要解析binlog认真确定要回滚的事务是哪个。

状况2:未开gtid的复制,此时比下面状况要麻烦,没有gtid来确定binlog事务是否反复,只有前面事务不波及到这半个xa事务锁定的资源,备机就能够失常维持复制体系,始终同步数据,等到有抵触数据呈现谬误,回放线程重试超过肯定次数后(slave_transaction_retries重试参数管制),sql线程报出相应谬误,复制中断后能力被感知。复原数据和下面差不多,回滚这个XA事务,重建主从,然而这个事务的binlog不肯定能找到,因为没有gtid不会立刻报错,可能几分钟后报错,也可能几个月后报错,取决于业务什么时候产生抵触数据。并且在这个事务之后,从机又同步了很多数据,这些数据是否牢靠须要评估。线上强烈建议开启Gtid复制模式,非gtid的复制官网曾经在淘汰!

3 分布式事务的一致性

应用到分布式事务,就必须要保障分布式事务的一致性。

分布式事务的一致性又分写一致性和读一致性,写一致性XA框架XA prepare 和XA commit曾经解决,只有保障有提交全提交,有回滚全回滚就能保障写一致性。

读一致性则要简单的多,先看看MySQL官网对XA事务在读一致性上的“只言片语”:

下面内容是从官网阐明文档里截取,外面对XA读一致性略有介绍:如果应用程序对读敏感,首选SERIALIZABLE隔离级别,RR级别不足以用于分布式事务,官网没有对这里的有余做具体阐明,但咱们能够构建一个例子来剖析这个“may not be sufficien”来形容读一致性是否失当。

如下图,有A、B两个账户在两个实例上,假如每个账户初始都100块,A给B转账20,工夫线右边为A账户实例上的操作,左边为B账户实例上的操作,两头T1到T6为不同工夫点。

T1时刻:初始均100。

T2时刻:AB账户均实现xa prepare操作,一个减20,一个加20。

T3时刻:A帐户节点XA commit胜利。

T5时刻:B帐户XA commit胜利。

当处在RR或RC隔离级别时,发动一个对账操作,统计AB帐户资金总额,当只有他们互相转账时,总金额应该恒为200。T6 时刻时,查问A为80,B为120,总账为200,无问题。T4时刻查问A账户为80,查问B账户时因为MVCC机制,会读到上个快照中的值100,加一起为180,总账不对。因为是操作不同实例,当开始做xa commit之后,可能因为网络等起因,并不能保障所有节点的XA commit同时达到所有节点,在一个高并发场景,导致下面的问题简直是必然的。因而,当应用MySQL 原生XA分布式事务时,若无其它伎俩来保障读一致性,而利用又有跨节点读的利用场景,该当应用序列化(SERIALIZABLE)隔离级别,“may not be sufficien”显然是不失当的,没有任何一个业务能承受这种数据统计不对的。

如果是序列化隔离级别,T4时刻读到A为80,读B时会期待,直到T5时刻XA commit胜利之后, 能力读到B为120,总账200,无问题。序列化隔离级别只有读-读不阻塞,读-写,写-读,写-写均会阻塞,而RC、RR仅写-写阻塞,因而只有序列化隔离级能力充沛保障MySQL XA事务的读一致性。但它阻塞太多,性能也是各种隔离级别中最差的,所以如无必要,通常不会应用这一隔离级别。业界有很多计划来解决分布式事务RR、RC下的读一致性问题,以进步数据库性能,但原生的MySQL不具备这种能力,因而应用MySQL原生XA事务的业务须要审慎抉择隔离级别。

4 小结

只有咱们小心面对残留XA事务,审慎解决Crash之后的可能存在的多余binlog数据,认真评估应用RR、RC隔离级别是否有读一致性读问题等问题之后,MySQL 的XA事务根本没有其它问题,能够作为RM齐备提供跨节点分布式事务能力,MySQL曾经实现了X/Open 组织定义的分布式事务处理标准中的语法性能,齐全能够释怀放业务在这条路上奔跑!

作者简介

Flyfox 高级后端工程师

从事数据库内核工作十多年,深度参加多个基于PostgreSQL、MySQL自研数据库我的项目,目前负责RDS产品研发团队工作。

获取更多精彩内容,请扫码关注[OPPO数智技术]公众号