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 数智技术] 公众号