分布式事务探究(一)

948 阅读9分钟

一、XA 规范

有个叫做X/Open的组织定义了分布式事务的模型,这里面有几个角色,就是AP(Application,应用程序),TM(Transaction Manager,事务管理器),RM(Resource Manager,资源管理器),CRM(Communication Resource Manager,通信资源管理器) ,其实Application说白了就是我们的系统,TM的话就是一个在系统里嵌入的一个专门管理横跨多个数据库的事务的一个组件,RM的话说白了就是数据库(比如MySQL),CRM可以是消息中间件(但是也可以不用这个东西) 。

然后这里定义了一个很重要的概念,就是全局事务,这个玩意儿说白了就是一个横跨多个数据库的事务,就是一个事务里,涉及了多个数据库的操作,然后要保证多个数据库中,任何一个操作失败了,其他所有库的操作全部回滚,这就是所谓的分布式事务 。

说白了,就是定义好的那个TM与RM之间的接口规范,就是管理分布式事务的那个组件跟各个数据库之间通信的一个接口,说白了就是这个意思 完了比如管理分布式事务的组件,TM就会根据XA定义的接口规范,跟各个数据库通信和交互,告诉大家说,一起来回滚一下,或者是一起来提交个事务,大概这个意思 这个XA仅仅是个规范,具体的实现是数据库产商来提供的,比如说MySQL就会提供XA规范的接口函数和类库实现,等等 。

二、2PC 理论

2PC说白了就是基于XA规范搞的一套分布式事务的理论,也可以叫做一套规范,或者是协议。Two-Phase-Commitment-Protocol,两阶段提交协议 2PC,其实就是基于XA规范,来让分布式事务可以落地,定义了很多实现分布式事务过程中的一些细节 。

2.1 准备阶段

简单来说,就是TM先发送个prepare消息给各个数据库,让各个库先把分布式事务里要执行的各种操作,先准备执行,其实此时各个库会差不多先执行好,就是不提交罢了 如果你硬是要理解一下的话,也可以认为是prepare消息一发,各个库先在本地开个事务,然后执行好SQL,万事俱备只欠东风了,而且注意这里各个数据库会准备好随时可以提交或者是回滚,有对应的日志记录的 然后各个数据库都返回一个响应消息给事务管理器,如果成功了就发送一个成功的消息,如果失败了就发送一个失败的消息 。

2.2 提交阶段

第一种情况,要是发现某个数据库失败了,那就尴尬了。或者是等了半天,某个数据库楞是死活不返回消息,跟失踪了一样,不知道在干嘛,也就麻烦了 这个时候就会直接判定这个分布式事务失败,毕竟某个数据库那里报错了,然后通知所有的数据库,全部回滚。

其实这里你可以认为是通知每个数据库,把自己本地的那个事务回滚不就得了,然后各个库都回滚好了以后就通知TM,TM就认为整个分布式事务都回滚了 但是呢,要是TM接收到所有的数据库返回的消息都是成功,那就直接发送个消息通知各个数据库说提交,然后各个数据库都在自己本地提交事务,提交好了通知下TM,TM要是发现所有数据库的事务都提交成功了,就认为整个分布式事务成功了。

2.3 图解

XA 和 2PC 规范.png

2.4 问题

同步堵塞问题

当 TM 给 RM 发送 prepare 消息之后,RM 就会在自己本地开启一个事务,执行需要执行的 SQL 语句,这时会锁定他要执行的资源,执行完毕后并不会将资源进行释放,而是将执行结果返回给 TM ,等待 TM 发送 commit 消息之后,RM 将事务提交,此时,才会将锁定的资源进行释放,那么这时如果有别的需要访问这个事务锁定的资源的话,就一直处于一个堵塞状态。

2PC 缺陷.png

TM 单点问题

TM 只有一个,那么 TM 挂了,那整个事务铁定直接凉凉。

事务状态丢失问题

TM 我们怕他故障,我们设置两台,假如说我们在给 RM1 发送完 commit 消息之后, TM1 挂掉了,我们重新选出出来一个新的 TM,叫 TM2,但这时的 TM2 是不知道现在事务是个什么状态,也不知道已经给哪些 RM 发送了 commit 信息的。

脑裂问题

这个的话就是说,如果我们有三个 RM,有两个已经收到了 commit 信息,进行了事务提交,但有一个 RM ,因为网络问题,没办法收到 commit 消息,数据就不一致了,这时,整个分布式事务也是凉凉的。

三、3PC 理论

其实 3PC 说白了,就是在解决 2PC 的一些问题并做一些优化

3.1 CanCommit 阶段

这个就是 TM 发送一个 CanCommit 消息给各个数据库,然后各个库返回个结果,这里的话呢,是不会执行实际的SQL语句的,就是各个库看看自己网络环境啊,各方面是否都正常。

3.2 PreCommit 阶段

如果各个库对 CanCommit 消息返回的都是成功,那么就进入PreCommit阶段,TM 发送 PreCommit 消息给各个库,这个时候就相当于2PC里的阶段一,其实就会执行各个SQL语句,只是不提交罢了;如果有个库对CanCommit消息返回了失败,TM 会发送 abort 消息给各个库,结束这个分布式事务。

3.3 DoCommit 阶段

如果各个库对PreCommit阶段都返回了成功,那么发送 DoCommit 消息给各个库,进行事务提交,各个库如果都返回提交成功给TM,那么分布式事务成功;如果有个库对PreCommit返回的是失败,或者超时一直没返回,那么TM认为分布式事务失败,直接发 abort 消息给各个库,进行回滚,各个库回滚成功之后通知TM,分布式事务回滚。

3.4 改进点

主要就是引入了 CanCommit 阶段,其次就是在 DoCommit 阶段,加上了超时机制,也就是说,如果一个库收到了 PreCommit 自己还返回成功了,如果超时时间到了,还没收到 TM 发送的 DoCommit 消息或者是 abort消息,直接判定为TM可能出故障了,会自己执行DoCommit操作,提交事务。

因为这里就是说,如果这个库接收到了PreCommit消息,说明第一阶段各个库对 CanCommit 都返回成功了,这样 TM 才会发送 PreCommit 来,那么就默认为基本上各个库的 PreCommit 都会成功,所以大家没接收到DoCommit,直接自己执行提交操作了所以这个超时的机制是基于 CanCommit 的引入来实现的,有了一个CanCommit多了一个阶段,大家才能自己执行超时 commit 机制,这不就解决了TM挂掉的单点问题么。

另外资源阻塞问题也得到了优化,因为一个库如果一直接收不到DoCommit消息,不会一直锁着资源,人家自己会提交释放资源的,所有能减轻资源阻塞问题,比2PC稍微好一些

3.5 缺陷

如果 TM 在 DoCommit 阶段发送了 abort 消息给各个库,结果因为脑裂问题,某个库没接收到 abort 消息,自己因为超时原因执行了 commit 操作,这样就会导致各个库之间的数据一致性问题。

四、MySQL 对 XA 的支持

这里的逻辑也是比较简单的,首先就是获取到各个库的链接地址,通过不同的库,创建出来相对应的 XAConnection ,XAResource ,设置分布式事务的 id , 设置上每个库事务的id,然后组装好自己要执行的 sql 语句,准备完成后,发送 prepare 消息,进行本地执行,对返回结果进行判断,如果都执行成功,就进入第二阶段 commit 进行提交,若有一个直接,则都进行回滚。

MySQL 的 XA 支持的是 2PC 的理论。

public static void main(String[] args) throws SQLException {
        // 创建商品库的RM实例
        Connection productConnection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/product", 
                "root", 
                "root");
        // 这里的这个true参数,是说打印出来XA分布式事务的一些日志
        XAConnection productXAConnection = new MysqlXAConnection(
                (com.mysql.jdbc.Connection)productConnection, true); 
        // 这个XAResource其实你可以认为是RM(Resource Manager)的一个代码中的对象实例
        XAResource productResource = productXAConnection.getXAResource();
        
        // 创建订单库的RM实例
        Connection orderConnection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/order", 
                "root", 
                "root");
        XAConnection orderXAConnection = new MysqlXAConnection(
                (com.mysql.jdbc.Connection)orderConnection, true);
        XAResource orderResource = orderXAConnection.getXAResource();
      
        // 下面俩东西是分布式事务id(txid)的构成部分
        byte[] ptrid = "p123".getBytes();
        int formatId = 1;
        
        try {
            // 这是说在分布式事务中的商品库的子事务的标识
            // 我们在商品库要执行的操作隶属于分布式事务的一个子事务,子事务有自己的一个标识
            byte[] bqual1 = "c001".getBytes();
            Xid xid1 = new MysqlXid(ptrid, bqual1, formatId); // 这个xid代表了商品库中的子事务
            
            // 这就是说通过START和END两个操作,定义好了分布式事务中,商品库中要执行的SQL语句
            // 但是这里的SQL绝对不会执行的,只是说先定义好我要在分布式事务中,这个数据库里要执行哪些SQL语句
            productResource.start(xid1, XAResource.TMNOFLAGS);
            PreparedStatement productPreparedStatement = productConnection.prepareStatement(
                    "UPDATE product SET name = '新增' WHERE id=1");
            productPreparedStatement.execute();
            productResource.end(xid1, XAResource.TMSUCCESS);
            
            // 这是说在分布式事务中的订单库的子事务的标识
            // 在一个分布式事务中,涉及到多个数据库的子事务,每个子事务的txid,有一部分是一样的,一部分是不一样的
            byte[] bqual2 = "c002".getBytes();
            Xid xid2 = new MysqlXid(ptrid, bqual2, formatId);
            // 这就是说通过START和END两个操作,定义好了分布式事务中,积分库中要执行的SQL语句
            orderResource.start(xid2, XAResource.TMNOFLAGS);
            PreparedStatement orderPreparedStatement = orderConnection.prepareStatement(
                    "UPDATE order SET POINT=POINT+1.2 WHERE id=1");
            orderPreparedStatement.execute();
            orderResource.end(xid2, XAResource.TMSUCCESS);
            
            // 到这里为止,其实还啥都没干呢,不过就是定义了分布式事务中的两个库要执行的SQL语句罢了
            
            // 2PC的阶段一:向两个库都发送prepare消息,执行事务中的SQL语句,但是不提交
            int productPrepareResult = productResource.prepare(xid1);
            int orderPrepareResult = orderResource.prepare(xid2);
            
            // 2PC的阶段二:两个库都发送commit消息,提交事务
            
            // 如果两个库对prepare都返回ok,那么就全部commit,对每个库都发送commit消息,完成自己本地事务的提交
            if (productPrepareResult == XAResource.XA_OK
                   && orderPrepareResult == XAResource.XA_OK) {
                productResource.commit(xid1, false);
                orderResource.commit(xid2, false);
            } 
            // 如果如果不是所有库都对prepare返回ok,那么就全部rollback
            else {
                productResource.rollback(xid1);
                orderResource.rollback(xid2);
            }
        } catch (XAException e) {
            e.printStackTrace();
        }
   }