分布式事物-- MySQL XA 协议

       上一篇博客《分布式事务--Fescar》分享了阿里巴巴开源的分布式事物框架Fescar,接触到Fescar的实现原理借鉴了XA协议,这篇博客我们来介绍总结一下XA协议。MySQL 从5.0.3开始支持XA分布式事务,且只有InnoDB存储引擎支持。MySQL Connector/J 从5.0.0版本之后开始直接提供对XA的支持。

标题

 分布式事物XA协议规范:

AP:应用程序,AP定义事务边界(定义事务开始和结束)并访问事务边界内的资源

RM:资源管理器,管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含比如数据库、文件系统、打印机服务器等。

TM:事物管理器,负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等。

Xa主要规定了RM与TM之间的交互,下面来看下XA规范中定义的RM 和 TM交互的接口:

  • xa_start负责开启或者恢复一个事务分支,并且管理XID到调用线程
  • xa_end 负责取消当前线程与事务分支的关联
  • xa_prepare负责询问RM 是否准备好了提交事务分支
  • xa_commit通知RM提交事务分支
  • xa_rollback 通知RM回滚事务分支

XA协议是使用了二阶段协议的,其中:

  • 第一阶段TM要求所有的RM准备提交对应的事务分支,询问RM是否有能力保证成功的提交事务分支,RM根据自己的情况,如果判断自己进行的工作可以被提交,那就就对工作内容进行持久化,并给TM回执OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经的工作后,就可以丢弃这个事务分支信息了。
  • 第二阶段TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚自己的事务分支。

XA协议二阶段提交的一个流程示意图:

扫描二维码关注公众号,回复: 5389106 查看本文章

也就是TM与RM之间是通过两阶段提交协议进行交互的。

内部XA事务 

        MySQL本身的插件式架构导致在其内部需要使用XA事务,此时MySQL即是协调者,也是参与者。例如,不同的存储引擎之间是完全独立的,因此当一个事务涉及两个不同的存储引擎时,就必须使用内部XA事务。需要特别注意的是,如果将二进制日志看做一个独立的“存储引擎”,就不难理解为什么即使是一个存储引擎参与的事务也需要使用XA事务了。在向存储引擎提交数据时,同时需要将提交的信息写入二进制日志,这就是一个分布式事务。

外部XA事务

1、分析:

MySQL数据库外部XA可以用在分布式数据库代理层,实现对MySQL数据库的分布式事务支持,例如开源的代理工具:ameoba[4],网易的DDB,淘宝的TDDL,B2B的Cobar等等。

  通过MySQL数据库外部XA,这些工具可以提供跨库的分布式事务。当然,这些工具也就成了外部XA事务的协调者角色。在crash recover时控制悬挂事务是全局commit,或者rollback。

  在crash recover之后,外部应用程序可能会遇到以下几种情况:

  情况一:分布式事务对应的MySQL数据库实例,部分完成prepare,部分未完成prepare。此时直接回滚完成prepare的实例即可。n_prepared < Total Nodes (处于prepare状态的节点数量要小于参与分布式事务的所有节点总数)。

  情况二:分布式事务对应的MySQL实例,全部完成prepare,未开始进行commit。此时即可提交此事务,也可回滚此事务(根据分布式事务原理,所有节点都完成prepare,应该提交)。n_prepared = Total Nodes。

  情况三:分布式事务对应的MySQL实例,全部完成prepare,并且部分节点已经完成commit。此时应该提交该事务处于prepare状态的节点。n_prepared < Total Nodes。对比情况三与情况一,仅仅通过prepare节点的数量无法区分,因此应用程序需要在prepare完成之后记录日志(此时,应用程序起着事务协调者(Transcaction Coordinator)的角色,而根据MariaDB WorkLog#132[5]的说法,TC角色是可以进行”middle engine”优化的,不需要prepare过程,所有MySQL节点xa prepare返回之后,应用程序直接写commit标识即可,然后再对每个MySQL节点进行xa commit操作。),从而用于区分情况一与情况三。

  情况四:分布式事务对应的MySQL实例,全部完成commit。此时事务已经提交成功,xid不会出现在执行xa recover的任一个节点。不需要特殊处理。

  情况五:未记录任何prepare日志。那么所有的事务,在各个存储引擎的crash recover时,都会被回滚,不需要外部特殊处理。

2、不足

通过前面的分析,可知应用程序配合MySQL的XA事务功能,能够较好的支持分布式环境下的事务。但是,这个支持并不完美,根据我的分析,有可能会出现以下几个问题:

问题一:主备数据库的数据不一致。

  MySQL数据库的主备数据库的同步,通过Binlog的复制完成。而Binlog是MySQL数据库内部XA事务的协调者,并且MySQL数据库为binlog做了优化——binlog不写prepare日志,只写commit日志。

  考虑前面提到的情况二,所有的参与节点prepare完成,在进行xa commit前crash。crash recover如果选择commit此事务。由于binlog在prepare阶段未写,因此主库中看来,此分布式事务最终提交了,但是此事务的操作并未写到binlog中,因此也就未能成功复制到备库,从而导致主备库数据不一致的情况出现。

  在MySQL 5.5.16版本中做过测试,这个问题实际存在。crash recover之后,对xa recover返回的事务运行xa commit,对应事务提交,但是操作并未写入binlog,因此无法复制到备库。

  那么是否回滚所有prepare的事务,就可以避免此问题呢?结论是仍旧不行,不仅不能解决问题一,甚至可能引起问题二。

问题二:同一事务,在各参与节点,最终状态不一致(部分提交,部分回滚)。

  若回滚所有prepare状态的分布式事务,会产生问题二。考虑情况三(所有节点完成prepare,部分节点完成commit),该分布式事务对应的节点,部分已经提交,无法回滚,而部分节点回滚。最终导致同一分布式事务,在各参与节点,最终状态不一致。

问题三:源码级别问题。MySQL 5.1.49源码对于外部XA事务处理存在bug,在MySQL 5.5.16版本中,此bug已经被fix。经过验证发现,在我已下载的MySQL 5.1.61与之后的所有版本,此bug均已经被fix。

示例:

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://192.168.0.1:3306/test";
        String connStr2 = "jdbc:mysql://192.168.0.2:3306/test";

        try {
            //从不同数据库获取数据库数据源
            MysqlXADataSource ds1 = getDataSource(connStr1, "root", "123456");
            MysqlXADataSource ds2 = getDataSource(connStr2, "root", "123456");

            //数据库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 account_from set money=money - 50 where id=1");
                xaResource1.end(xid1, XAResource.TMSUCCESS);

                //事务分支2关联分支事务sql语句
                xaResource2.start(xid2, XAResource.TMNOFLAGS);
                int update2Result = statement2.executeUpdate("update account_to set money= money + 50 where id=1");
                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();
        }
    }

}

脑图: 

标题

引用:

http://ifeve.com/mysql-xa/

https://www.cnblogs.com/duanxz/p/4717500.html

https://blog.csdn.net/OnlyQi/article/details/50570439

https://www.jianshu.com/p/6c1fd2420274

猜你喜欢

转载自blog.csdn.net/qq924862077/article/details/86583006
今日推荐