一、分布式事务
在说分布式事务(XA)之前,建议可以先看一下分布式事务架构的五大演进,阐述了分布式事务解决了什么问题?
InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高。
XA事务语允许不同数据库之间的分布式事务,如一台服务器是MySQL数据库的,另一台是Oracle数据库的,又可能还有一台服务器是SQL Server数据库的,只要参与在全局事务中的每个节点都支持XA事务。
我们考虑下面一种场景:当你发了工资之后,把你的当月工资¥10000从银行卡转到了支付宝中。如果在银行卡账户扣除¥10000之后,支付宝系统挂掉了,支付宝的账户并没有增加¥10000,这时候就出现了数据不一致的情况。(当然这种情况肯定不会出现了,只是一个假设)
在很多交易系统中(比如我们熟知的电商)都能找到上述情况的影子:
- 在下单的时候,需要在订单表中插入一条数据,然后把库存减去一。
- 在搜索的时候如果点击了广告,需要先记录该点击事件,然后通知商家系统扣除广告费。
在这种情况下,一定需要使用分布式事务来保证数据的安全。如果发生的操作不能全部提交或回滚,那么任何一个节点出现问题都会导致严重的结果。
可见与本地事务不同的是,分布式事务需要多一次的PREPARE操作,待收到所有节点的同意信息后,再进行COMMIT或是ROLLBACK操作。
现如今实现基于两阶段提交的分布式事务也没那么困难了,如果使用java,那么可以使用开源软件atomikos(http://www.atomikos.com/)或者TCC开源的框架来快速实现。
- 不过但凡使用过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合高并发的系统。为什么?
两阶段提交涉及多次节点间的网络通信,通信时间太长! - 事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多!
正是由于分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用,往往通过其他途径来解决数据一致性问题。比如使用消息队列来避免分布式事务。
二、MySQL分布式事务操作
1、XA事务语法
# 在mysql实例中开启一个XA事务,指定一个全局唯一标识; mysql> XA START 'any_unique_id'; # XA事务的操作结束; mysql> XA END 'any_unique_id'; # 告知mysql准备提交这个xa事务; mysql> XA PREPARE 'any_unique_id'; # 告知mysql提交这个xa事务; mysql> XA COMMIT 'any_unique_id'; # 告知mysql回滚这个xa事务; mysql> XA ROLLBACK 'any_unique_id'; # 查看本机mysql目前有哪些xa事务处于prepare状态; mysql> XA RECOVER;
2、XA事务演示
在单个节点上运行分布式事务是没有意义的,起码两个节点才有意义。但是要在MySQL数据库的命令行下演示多个节点参与的分布式事务也是行不通的。通常来说,都是通过编程语言来完成分布式事务操作的。当前Java的JTA可以很好地支持MySQL的分布式事务
下面通过JAVA代码用一个简单的例子来演示:
import java.sql.Connection; import java.sql.Statement; import javax.sql.XAConnection; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource; import com.mysql.jdbc.jdbc2.optional.MysqlXid; public class XaDemo { public static MysqlXADataSource getDataSource(String connStr, String user, String pwd) { try { MysqlXADataSource ds = new MysqlXADataSource(); ds.setUrl(connStr); ds.setUser(user); ds.setPassword(pwd); return ds; } catch (Exception e) { e.printStackTrace(); } return null; } public static void main(String[] arg) { String connStr1 = "jdbc:mysql://118.89.107.162:3306/wjq"; String connStr2 = "jdbc:mysql://118.89.107.162:3307/wjq"; try { //从不同数据库获取数据库数据源 MysqlXADataSource ds1 = getDataSource(connStr1, "root", "XXXXXXXX"); MysqlXADataSource ds2 = getDataSource(connStr2, "root", "XXXXXXXX"); //数据库1获取连接 XAConnection xaConnection1 = ds1.getXAConnection(); XAResource xaResource1 = xaConnection1.getXAResource(); Connection connection1 = xaConnection1.getConnection(); Statement statement1 = connection1.createStatement(); //数据库2获取连接 XAConnection xaConnection2 = ds2.getXAConnection(); XAResource xaResource2 = xaConnection2.getXAResource(); Connection connection2 = xaConnection2.getConnection(); Statement statement2 = connection2.createStatement(); //创建事务分支的xid Xid xid1 = new MysqlXid(new byte[] { 0x01 }, new byte[] { 0x02 }, 100); Xid xid2 = new MysqlXid(new byte[] { 0x011 }, new byte[] { 0x012 }, 100); try { //事务分支1关联分支事务sql语句 xaResource1.start(xid1, XAResource.TMNOFLAGS); int update1Result = statement1.executeUpdate("UPDATE accounts SET BALANCE = CAST('9700.00' AS DECIMAL) WHERE CUSTOMER_NO = '001'"); xaResource1.end(xid1, XAResource.TMSUCCESS); //事务分支2关联分支事务sql语句 xaResource2.start(xid2, XAResource.TMNOFLAGS); int update2Result = statement2.executeUpdate("INSERT INTO user_purchase_his(CUSTOMER_NO, SERIAL_NO, AMOUNT, CURRENCY, REMARK) " + " VALUES ('001', '20190303204700000001', 200, 'CNY', '购物消费')"); xaResource2.end(xid2, XAResource.TMSUCCESS); // 两阶段提交协议第一阶段 int ret1 = xaResource1.prepare(xid1); int ret2 = xaResource2.prepare(xid2); // 两阶段提交协议第二阶段 if (XAResource.XA_OK == ret1 && XAResource.XA_OK == ret2) { xaResource1.commit(xid1, false); xaResource2.commit(xid2, false); System.out.println("reslut1:" + update1Result + ", result2:" + update2Result); } } catch (Exception e) { e.printStackTrace(); } } catch (Exception e) { e.printStackTrace(); } } }
程序执行结果:
3、XA事务恢复
如果执行分布式事务的某个mysql crash了,MySQL按照如下逻辑进行恢复:
a. 如果这个xa事务commit了,那么什么也不用做。
b. 如果这个xa事务还没有prepare,那么直接回滚它。
c. 如果这个xa事务prepare了,还没commit,那么把它恢复到prepare的状态,由用户去决定commit或rollback。
当mysql crash后重新启动之后,执行“XA RECOVER;”查看当前处于prepare状态的xa事务,然后commit或rollback它们即可。如果不去处理,那么它们占用的资源就一直不会释放,比如锁。
三、MySQL分布式事务限制
1、XA事务和本地事务以及锁表操作是互斥的
开启了xa事务就无法使用本地事务和锁表操作
root@localhost [3306][wjq]>xa start 'xatest'; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]>begin; ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state root@localhost [3306][wjq]>lock table xatest read; ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
开启了本地事务就无法使用xa事务
root@localhost [3306][wjq]>begin; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]>xa start 'xatest'; ERROR 1400 (XAE09): XAER_OUTSIDE: Some work is done outside global transaction
2、xa start之后必须xa end,否则不能执行xa commit和xa rollback
所以如果在执行xa事务过程中有语句出错了,你也需要先xa end一下,然后才能xa rollback。
root@localhost [3306][wjq]>xa start 'xatest'; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]>xa rollback 'xatest'; ERROR 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state root@localhost [3306][wjq]>xa end 'xatest'; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]>xa rollback 'xatest'; Query OK, 0 rows affected (0.00 sec)
四、MySQL 5.7对分布式事务的支持
一直以来,MySQL数据库是支持分布式事务的,但是只能说是有限的支持,具体表现在:
- 已经prepare的事务,在客户端退出或者服务宕机的时候,2PC的事务会被回滚。
- 在服务器故障重启提交后,相应的Binlog被丢失。
上述问题存在于MySQL数据库长达数十年的时间,直到MySQL-5.7.7版本,官方才修复了该问题。下面将会详细介绍下该问题的具体表现和官方修复方法,这里分别采用官方MySQL-5.6.27版本(未修复)和MySQL-5.7.9版本(已修复)进行验证。
注意:这里的MYSQL的测试版本是5.6版本,而非5.7版本,如果使用5.7版本测试,是无法复现下面的问题的;
先来看下存在的问题,我们先创建一个表如下:
root@localhost [3306][wjq]>create table xatest(id int auto_increment primary key,col1 int);
Query OK, 0 rows affected (0.01 sec)
对于上述表,通过如下操作进行数据插入:
root@localhost [3306][wjq]> XA START 'wjqmysql56'; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]> INSERT INTO xatest VALUES(1,1); Query OK, 1 row affected (0.02 sec) root@localhost [3306][wjq]> XA END 'wjqmysql56'; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]> XA PREPARE 'wjqmysql56'; Query OK, 0 rows affected (0.00 sec)
通过上面的操作,用户创建了一个分布式事务,并且prepare没有返回错误,说明该分布式事务可以被提交。通过命令XA RECOVER查看显示如下结果:
root@localhost [3306][wjq]> XA RECOVER; +----------+--------------+--------------+------------+ | formatID | gtrid_length | bqual_length | data | +----------+--------------+--------------+------------+ | 1 | 7 | 0 | wjqmysql56 | +----------+--------------+--------------+------------+
若这时候用户退出客户端后重连,通过命令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.6及以前版本MySQL在分布式事务上的各种问题,那么MySQL 5.7版本官方做了哪些改进?这个可以从官方的WL#6860描述上得到一些信息,我们还是本着没有实践就没有发言权的态度,从具体的操作上来分析下MySQL 5.7的改进方法。还是以上面同样的表结构进行同样的操作如下:
root@localhost [3306][wjq]>xa start 'wjqmysql5.7'; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]>insert into xatest values(1,1); Query OK, 1 row affected (0.00 sec) root@localhost [3306][wjq]>xa end 'wjqmysql5.7'; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]>xa prepare 'wjqmysql5.7'; Query OK, 0 rows affected (0.00 sec)
这个时候,我们通过mysqlbinlog来查看下Master上的Binlog,结果如下:
同时也对比下Slave上的Relay log,如下:
通过上面的操作,明显发现在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的回放?
其实这个实现也很简单,只要在SQL thread回放到prepare的时候,进行类似于客户端断开连接的处理即可(把相关cache与SQL thread的连接句柄脱离)。最后在Slave服务器上,用户通过命令XA RECOVER可以查到如下信息:
至于上面的事务什么时候提交,一般等到Master上进行XA COMMIT ‘wjqmysql5.7’后,slave上也同时会被提交。
root@localhost [3306][wjq]>xa recover; +----------+--------------+--------------+-------------+ | formatID | gtrid_length | bqual_length | data | +----------+--------------+--------------+-------------+ | 1 | 11 | 0 | wjqmysql5.7 | +----------+--------------+--------------+-------------+ 1 row in set (0.00 sec)
xa事务提交之后,就可以在从库那边看到数据了
root@localhost [3306][wjq]>xa commit 'wjqmysql5.7'; Query OK, 0 rows affected (0.00 sec) root@localhost [3306][wjq]> root@localhost [3306][wjq]>xa recover; Empty set (0.00 sec) root@localhost [3308][wjq]>select * from xatest; +----+------+ | id | col1 | +----+------+ | 1 | 1 | +----+------+ 1 row in set (0.00 sec)
小结
综上所述,MySQL 5.7对于分布式事务的支持变得完美了,因而又多了一个升级到MySQL 5.7版本的理由,所以生产环境中建议使用5.7版本吧!!!