这一篇,我们将跟踪dubbo远程服务执行业务代码!

  • 在我们的实际业务中,普遍使用了微服务框架,meepo-test里本地业务sql和dubbo业务接口需要共享事务的场景非常多,二阶段提交协议比较知名的Atomikos框架,只能处理一个服务+多个数据库源的场景,并没有提供微服务场景的实现。

    那我们怎么处理这种情况呢?其实思路很简单,可以按照JTA的规范来。JTA里的XAResource,并不仅仅是数据库连接,其他连接比如RPC接口、MQ都可以实现XAResource接口,从而被JTA事务管理。

    在XA/JTA协议的实现里,客户端本地事务里的一组DML操作,或者一个本地XA连接对应一个XAResource。XAResource 是 Distributed Transaction Processing: The XA Specification 标准的 Java 实现,它是对底层事务资源的抽象,定义了分布式事务处理过程中事务管理器和资源管理器之间的协议,各事务资源提供商(如 JDBC 驱动,JMS)将提供此接口的实现。使用此接口,开发人员可以通过自己的编程实现分布式事务处理,但这些通常都是由应用服务器实现的(服务器自带实现更加高效,稳定) 为了说明,我们将举例说明他的使用方式

    比如mysql实现XAResource的实现类MysqlXAConnection,看一下prepare方法

    MysqlXAConnection

    最终用jdbc来下发XA PREPARE命令

    jdbc下发

    下图是一次genenral_log一次完整的XA事务sql命令记录

    XA事务sql记录

    搞清楚了数据库实现XAResource的流程,接下来我们分析下dubbo服务如何实现JTA里的XAResource接口

  • 一、dubbo服务实现XAResource

    实现XAResource接口则必须实现XAResource下面的几个方法,如start、end、prepare、commit等方法

    但问题是无论是dubbo还是其他微服务,都只有一个业务方法,这种情况该如何来扩展呢?

    在深入了解byteJta或者阿里GTS前,一直觉得,除了接口提供方和接口消费者,还得有一个第三方中间件来协调全局事务, 因为消费者承担不了这个角色。我们有理由这样想,在物理上消费者连接不到提供者的数据库,如何来协调事务提交或回滚

    持有这个观点的,除了开始研究时本人和同事的观点。还有下面这位

    微服务调用虽然只有一个业务方法,但是实现微服务的Rpc框架一般都可以扩展,比如dubbo的filter。dubbo的filter既可以在provider端扩展, 也可以在cosumer端扩展。在cosumer调用业务方法前,消费者filter可以写一个方法,向provider发出一个start请求,返回后执行业务,业 务完成后再发送prepare、commit或者rollback请求,理论上并不难理解

    既然要发送和接收业务外请求,我们必须在服务启动的时候,动态生成及初始化一个dubbo的事务专用service和reference,来执行这些任务

    meepo 的provider服务启动后,可以看到dubbo服务监控如下

    事务专用service

    RemoteCoordinator继承了XAResource接口,所以包含XAResource所有方法

    RemoteCoordinator

    再来看一下RemoteCoordinator如何生成。在jta-supports-dubbo-core.xml(spring配置文件引入)里,定义的TransactionConfigPostProcessor扩展了spring的BeanFactoryPostProcessor接口

    我们知道,Spring IoC容器允许BeanFactoryPostProcessor在容器实例化任何bean之前读取bean的定义(配置元数据),并可以修改它

    初始化RemoteCoordinator

    初始化过程

    上面这段meepo代码,对配置文件的dubbo的bean类型和group做了判断,只有在真正需要分布式事务的时候,才会加载事务service

    在bytejta里RemoteCoordinator的方法和远程服务的本地XAResource方法大致对应,这是因为两阶段提交协议的特性,事务管理器通过dubbo的方法调用,来控制远程服务XA命令

    关系大致如下:TxManager.prepare = 1、consumer.MysqlXAConnection.prepare+2、RemoteCoordinator.prepare ->provider.MysqlXAConnection.prepare

    在meepo里,RemoteCoordinator作用弱化,这是因为远程服务业务执行完成后,直接XA prepare+commit,不需要事务管理器再统一发送prepare

  • 二、源码追踪

    阅读源码前,我们先看一下dubbo服务实现XAResource的流程图

    dubbo jta

    在我们的测试用例里,dubbo调用服务只有一句

    this.remoteAccountService.decreaseAmount(sourceAcctId, amount);

    但是在流程图里,这句代码实际执行的事件非常多,包含多个请求来回,这些额外的事件是由dubbo提供的扩展点filter实现

    先简单介绍下dubbo的设计(官网)

    dubbo对服务调用的过程进行了分层设计,使用动态代理对调用过程层层封装,以达到每个模块单一职责的设计目标

    dubbo在服务启动解析bean的时候,初始化一个invoker链,每个invoker对应一层的可执行体,可执行体实现Invoker接口,实现invoke方法

    所以我们在跟踪dubbo的代码时,经常跳到一个Invoker,完成当前层的任务后,又跳到下一个Invoker里

    dubbo中供开发者扩展的filter,也可视作一个分层,虽然没有实现Invoker接口,但也有invoke方法

    1、调试进入remoteAccountService.decreaseAmount,第一层是MockClusterInvoker的代理类,除了对toString等Object方法进行本地处理,主要初始化了RpcInvocation对象,Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等

    Invoker调用链

    2、然后进入MockClusterInvoker的invoker方法,判断bean里是否有配置mock,如有,执行doMockInvoke,然后进入下一个Invoker

    MockClusterInvoker的作用主要是服务降级,invoker链的下一个是FailfastClusterInvoker

    Invoker调用链

    关于dubbo的原理流程,有兴趣的读者可以自行研究,这里不再叙述,代码直接跳到filter的Invoker方法,filter属于protocol远程调用层

    3、根据分层原理,dubbo的filter执行的时机是:cosumer-filter -> 调用远程服务 ->provider-filter->provider执行业务

    首先进入的是consumer的fileter

    Invoker调用链-filter

    进入业务service前的处理,在consumerInvokeForSVC方法中,做了一些初始化bean的工作

    consumerInvokeForSVC

    4、我们关注下调用远程接口前的beforeConsumerInvokeForSVC方法

    consumerInvokeForSVC

    consumerInvokeForSVC

    beforeSendRequest封装了流程图里的enlist方法

    enlist

    5、enlist方法做了两件事,1、把当前remoteXAResource加入全局事务管理;2、调用远程start方法

    enlist

    enlist

    6、继续进入start方法,除了执行一些之前动态代理不重要的操作,又进入了我们熟悉的dubbo第一层Invoker调用链,这也符合前面流程图的步骤,start方法先于业务方法

    start方法进入到consumer的filter后,做了一些上下文内容的处理,就进入了provider的filter,provider的filter及RemoteCoordinator接口的 start方法中,也仅仅把上下文中传入的事务,绑定到当前线程

    流程图中,dubbo接口的start方法,并未和provider本地数据库的start方法同步,因为直到真正执行业务方法,才能获取到provider端的XA连接

    这里需要说明的是,xa中的start和end,代表了xa事务管理的边界,而不是开始和结束事务的意思,xa执行到end方法后,相应的记录并不会锁定。真正的开始事务是prepare、结束则是commit或rollback,期间数据库资源被锁定

    我们整理下当前 已执行的步骤

    dubbo业务调用(未执行)-> enlist(DubboXAResource)->RemoteCoordinator.start->start返回

    7、接下来进入到dubbo业务方法的filter,执行步骤:执行dubbo业务->provider本地XA prepare->provider本地XA commit->线程解绑事务

    以上步骤是meepo和bytejta的根本不同之处,bytejta在业务执行完后直接返回consumer,provider等待下一次cosumer调用的prepare指令, 而meepo直接一步到位,执行完xa所有流程,数据库资源得到释放

    meepo的consumer在得到业务返回的结果后,如果有异常,则全局事务发布统一回滚,对dubbo服务来说,即调用RemoteCoordinator接口的rollback

    meepo从持久化到数据库表txc_undo_log中的前后镜像信息,进行回滚操作

    如果没有异常,则全局事务发布统一commit,异步清理txc_undo_log表信息及锁表信息

    关于txc_undo_log中的前后镜像信息如何生成,会在后面文章中体现

    如果你够仔细的话,会发现在整个流程中,XA start 和end还没有执行,这两个步骤是在执行业务的时候,获取到数据库连接enlist阶段的时候执行,见前文最后处

    无论是哪个服务,在执行服务中分支事务的时候,都会在dbcp2 enlist阶段,在服务的数据库连接中执行start和end方法