• 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

MySQL分布式事务

MySQL 彭东稳 8年前 (2017-04-17) 30327次浏览 已收录 0个评论

一、分布式事务

在说分布式事务(XA)之前,可以先看一下“以交易系统为例,看分布式事务架构的五大演进”,阐述了分布式事务解决了什么问题?

InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高。另外,在使用分布式事务时,InnoDB存储引擎的事务隔离级别必须设置为SERIALIZABLE。

XA事务语允许不同数据库之间的分布式事务,如一台服务器是MySQL数据库的,另一台是Oracle数据库的,又可能还有一台服务器是SQL Server数据库的,只要参与在全局事务中的每个节点都支持XA事务。

考虑下面一种场景:当你发了工资之后,把你的当月工资¥10000从支付宝转到了余额宝。如果在支付宝账户扣除¥10000之后,余额宝系统挂掉了,余额宝的账户并没有增加¥10000,这时候就出现了数据不一致的情况。

在很多系统中都能找到上述情况的影子:

  • 在下单的时候,需要在订单表中插入一条数据,然后把库存减去一。
  • 在搜索的时候如果点击了广告,需要先记录该点击事件,然后通知商家系统扣除广告费。

在这种情况下,一定需要使用分布式事务来保证数据的安全。如果发生的操作不能全部提交或回滚,那么任何一个节点出现问题都会导致严重的结果。

NOTE

在一个分布式事务结束的时候,事务的原子特性要求所有参与该事务的服务器必须全部提交或全部放弃该事务。为了实现这一点,其中一个服务器承担了协调者(coordinater)的角色,由它来保证所有的服务器获得相同的结果。

协调者(coordinater)的工作方式取决于它选用的协议,“两阶段提交”是分布式事务最常用的协议。

两阶段提交协议(Two-phase Commit,2PC)经常被用来实现分布式事务。一般由一个或多个资源管理器(resource managers)、一个事务协调器(transaction coordinater)以及一个应用程序(application program)组成。事务协调器可以和资源管理器在一台机器上。

  • 资源管理器:提供访问事务资源的方法,通常一个数据库就是一个资源管理器。
  • 事务协调器:协调参与全局事务中的各个事务,需要和参与全局事务的所有资源管理器进行通信。
  • 应用程序:定义事务的边界,指定全局事务中的操作。

在MySQL数据库的分布式事务中,资源管理器就是MySQL数据库,事务协调器为连接MySQL服务器的客户端(支持分布式事务的客户端)。下图显示了一个分布式事务的模型。

MySQL分布式事务

分布式事务通常采用2PC协议,全称Two Phase Commitment Protocol。该协议主要为了解决在分布式数据库场景下,所有节点间数据一致性的问题。在分布式事务环境下,事务的提交会变得相对比较复杂,因为多个节点的存在,可能存在部分节点提交失败的情况,即事务的ACID特性需要在各个数据库实例中保证。总而言之,在分布式提交时,只要发生一个节点提交失败,则所有的节点都不能提交,只有当所有节点都能提交时,整个分布式事务才允许被提交。

在该协议的第一个阶段,每个参与者投票表决该事务是放弃还是提交,一旦参与者要求提交事务,那么就不允许放弃该事务。因此,在一个参与者要求提交事务之前,它必须保证最终能够执行分布式事务中自己的那部分,即使该参与者出现故障而被中途替换掉。

一个事务的参与者如果最终能提交事务,那么可以说参与者处于事务的准备好prepared)状态。为了保证能够提交,每个参与者必须将事务中所有发生改变的对象以及自身的状态prepared)保存到持久性存储中。

在该协议的第二个阶段,事务的每个参与者执行最终统一的决定。如果任何一个参与者投票放弃事务,那么最终的决定是放弃事务,则所有的节点都被告知需要回滚。如果所有的参与者都投票提交事务,那么最终的决定是提交事务。

NOTE

1. 我们的应用程序(client)发起一个开始请求到TC;

2. TC先将<prepare>消息写到本地日志,之后向所有的RM发起<prepare>消息。以支付宝转账到余额宝为例,TC给A的prepare消息是通知支付宝数据库相应账目扣款1万,TC给B的prepare消息是通知余额宝数据库相应账目增加1w。为什么在执行任务前需要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证的效果,如果没有本地日志(凭证),出问题容易死无对证;

3. RM收到<prepare>消息后,执行具体本机事务,但不会进行commit,如果成功返回<yes>,不成功返回<no>。同理,返回前都应把要返回的消息写到日志里,当作凭证。

4. TC收集所有执行器返回的消息,如果所有执行器都返回yes,那么给所有执行器发生送commit消息,执行器收到commit后执行本地事务的commit操作;如果有任一个执行器返回no,那么给所有执行器发送rollback消息,执行器收到rollback消息后执行事务rollback操作。

注:TC或RM把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。如某一RM从故障中恢复后,先检查本机的日志,如果已收到<commit >,则提交,如果<rollback>则回滚。如果是<yes>,则再向TC询问一下,确定下一步。如果什么都没有,则很可能在<prepare>阶段RM就崩溃了,因此需要回滚。

 

可见与本地事务不同的是,分布式事务需要多一次的PREPARE操作,待收到所有节点的同意信息后,再进行COMMIT或是ROLLBACK操作。

现如今实现基于两阶段提交的分布式事务也没那么困难了,如果使用java,那么可以使用开源软件atomikos(http://www.atomikos.com/)来快速实现。

不过但凡使用过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合高并发的系统。为什么?

  • 两阶段提交涉及多次节点间的网络通信,通信时间太长!
  • 事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多!

正是由于分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用,往往通过其他途径来解决数据一致性问题。比如使用消息队列来避免分布式事务。

二、MySQL分布式事务操作

要在 MySQL 中执行 XA 事务,请使用以下语句:

对于 XA START,可以识别 JOIN 和 RESUME 子句,但无效。

对于 XA END,可以识别 SUSPEND [FOR MIGRATE] 子句,但无效。

每个 XA 语句都以 XA 关键字开头,而且它们中的大多数都需要 XID 值,XID 是 XA 事务标识符。它表示该声明适用的事务,XID 值由客户端提供,或由 MySQL Server 生成。 XID 值从一到三个部分起:

gtrid 是一个全局事务标识符,bqual 是分支限定符,并且 formatID 是标识 gtrid 和 bqual 值所使用的格式的数字。如语法所示, bqual 和 formatID 是可选的。如果未指定,默认 bqual 值为 ”。如果未指定,则默认值 formatID 为 1。

gtrid 和 bqual 必须是字符串 Literals,每个字符串的长度最多为 64 个字节(不是字符)。 gtrid 和 bqual 可以通过几种方式指定。你可以使用带引号的字符串 (‘ab’),十六进制字符串 (X’6162’,0x6162) 或位值 (b’nnnn’)。

formatID 是无符号整数。

gtrid 和 bqual 值由 MySQL 服务器的基础 XA 支持例程以字节为单位进行解释。但是,在解析包含 XA 语句的 SQL 语句时,服务器将使用某些特定的字符集。为了安全起见,将 gtrid 和 bqual 写为十六进制字符串。

xid 值通常由事务 Management 器生成。一个 Management 生成的值必须不同于其他 Management 生成的值。给定的 Management 必须能够识别 XA RECOVER 语句返回的值列 table 中自己的 xid 值。

XA START xid 使用给定的 xid 值开始 XA 事务。每个 XA 事务必须具有唯一的 xid 值,因此该值当前不能被另一个 XA 事务使用。使用 gtrid 和 bqual 值评估唯一性。必须使用与 XA START 语句中给定的相同值 xid 来指定用于 XA 事务的所有以下 XA 语句。如果使用这些语句中的任何一条,但是指定的 xid 值与某些现有 XA 事务不对应,则将发生错误。

一个或多个 XA 事务可以是同一全局事务的一部分。给定全局事务中的所有 XA 事务必须在 xid 值中使用相同的 gtrid 值。因此,gtrid 值必须是全局唯一的,以使给定 XA 事务属于哪个全局事务没有歧义。 xid 值的 bqual 部分对于全局事务中的每个 XA 事务必须不同。(bqual 值必须不同,这是当前 MySQL XA 实现的限制.它不是 XA 规范的一部分)

XA RECOVER 语句返回 MySQL 服务器上处于 PREPARED 状态的 XA 事务的信息。无论服务器是由哪个 Client 端启动的,输出都会为服务器上的每个此类 XA 事务包含一行。

XA RECOVER 输出行看起来像这样(例如 xid 由 ‘abc’,’def’ 和 7 组成的值):

输出列具有以下含义:

  • formatID 是 Transaction 的 formatID 部分 xid。
  • gtrid_length 是 xid 的 gtrid 部分的字节长度。
  • bqual_length 是 xid 的 bqual 部分的字节长度。
  • data 是 xid 的 gtrid 和 bqual 部分的串联。

XID 值可能包含不可打印的字符。从 MySQL 5.7.5 开始,XA RECOVER 允许使用可选的 CONVERT XID 子句,以便 Client 端可以请求十六进制的 XID 值。

XA事务演示

在单个节点上运行分布式事务是没有意义的,起码两个节点才有意义。但是要在MySQL数据库的命令行下演示多个节点参与的分布式事务也是行不通的。通常来说,都是通过编程语言来完成分布式事务操作的。当前Java的JTA可以很好地支持MySQL的分布式事务。下面用一个简单的例子来演示:

XA事务恢复

如果执行分布式事务的某个 MySQL Crash 了,MySQL 按照如下逻辑进行恢复:

  1. 如果这个 XA 事务 commit 了,那么什么也不用做。
  2. 如果这个 XA 事务还没有 prepare,那么直接回滚它。
  3. 如果这个 XA 事务 prepare 了,还没 commit,那么把它恢复到 prepare 的状态,由用户去决定 commit 或 rollback。

当 mysql crash 后重新启动之后,执行 XA RECOVER 查看当前处于 prepare 状态的 XA 事务,然后 commit 或 rollback 它们即可。如果不去处理,那么它们占用的资源就一直不会释放,比如锁。

三、MySQL分布式事务限制

a. XA事务和本地事务以及锁表操作是互斥的

开启了xa事务就无法使用本地事务和锁表操作

开启了本地事务就无法使用xa事务

b. xa start之后必须xa end,否则不能执行xa commit和xa rollback

所以如果在执行xa事务过程中有语句出错了,你也需要先xa end一下,然后才能xa rollback。

四、MySQL 5.7对分布式事务的支持

一直以来,MySQL数据库是支持分布式事务的,但是只能说是有限的支持,具体表现在:

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

上述问题存在于MySQL数据库长达数十年的时间,直到MySQL-5.7.7版本,官方才修复了该问题。下面将会详细介绍下该问题的具体表现和官方修复方法,这里分别采用官方MySQL-5.6.27版本(未修复)和MySQL-5.7.9版本(已修复)进行验证。

先来看下存在的问题,我们先创建一个表如下:

对于上述表,通过如下操作进行数据插入:

通过上面的操作,用户创建了一个分布式事务,并且prepare没有返回错误,说明该分布式事务可以被提交。通过命令XA RECOVER查看显示如下结果:

若这时候用户退出客户端后重连,通过命令xa recover会发现刚才创建的2PC事务不见了。即prepare成功的事务丢失了,不符合2PC协议规范!!!

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

官方的做法,貌似干得很漂亮,牺牲了一点标准化的东西,至少保证了主从数据的一致性。但其实不然,若用户已经prepare后在客户端退出之前,MySQL发生了宕机,这个时候又会怎样?

MySQL在某个分布式事务prepare成功后宕机,宕机前操作该事务的连接并没有断开,这个时候已经prepare的事务并不会被回滚,所以在MySQL重新启动后,引擎层通过recover机制能恢复该事务。当然该事务的Binlog已经在宕机过程中被丢失,这个时候,如果去提交,则会造成主从数据的不一致,即提交没有记录Binlog,从上丢失该条数据。所以对于这种情况,官方一般建议直接回滚已经prepare的事务。

以上是MySQL 5.7以前版本MySQL在分布式事务上的各种问题,那么MySQL 5.7版本官方做了哪些改进?这个可以从官方的WL#6860描述上得到一些信息,我们还是本着没有实践就没有发言权的态度,从具体的操作上来分析下MySQL 5.7的改进方法。还是以上面同样的表结构进行同样的操作如下:

这个时候,我们通过mysqlbinlog来查看下Master上的Binlog,结果如下:

MySQL分布式事务

同时也对比下Slave上的Relay log,如下:

MySQL分布式事务

通过上面的操作,明显发现在prepare以后,从XA START到XA PREPARE之间的操作都被记录到了Master的Binlog中,然后通过复制关系传到了Slave上。也就是说MySQL 5.7开始,MySQL对于分布式事务,在prepare的时候就完成了写Binlog的操作,通过新增一种叫XA_prepare_log_event的event类型来实现,这是与以前版本的主要区别(以前版本prepare时不写Binlog)。

当然仅靠这一点是不够的,因为我们知道Slave通过SQL thread来回放Relay log信息,由于prepare的事务能阻塞整个session,而回放的SQL thread只有一个(不考虑并行回放),那么SQL thread会不会因为被分布式事务的prepare阶段所阻塞,从而造成整个SQL thread回放出现问题?这也正是官方要解决的第二个问题:怎么样能使SQL thread在回放到分布式事务的prepare阶段时,不阻塞后面event的回放?其实这个实现也很简单(在xa.cc::applier_reset_xa_trans),只要在SQL thread回放到prepare的时候,进行类似于客户端断开连接的处理即可(把相关cache与SQL thread的连接句柄脱离)。最后在Slave服务器上,用户通过命令XA RECOVER可以查到如下信息:

至于上面的事务什么时候提交,一般等到Master上进行XA COMMIT  ‘mysql57’后,slave上也同时会被提交。

总结

综上所述,MySQL 5.7对于分布式事务的支持变得完美了,一个长达数十年的bug又被修复了,因而又多了一个升级到MySQL 5.7版本的理由。

<延伸>

distributed transaction

一篇文章带你学习分布式事务

如何用消息系统避免分布式事务?

https://blog.csdn.net/sun_ashe/article/details/103674405


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (2)
[资助本站您就扫码 谢谢]
分享 (0)

您必须 登录 才能发表评论!