这一篇,我们将根据业务时序图,来跟踪代码!

  • 先回顾下meepo正常提交的时序图

    结构图

  • 第一步:事务发起方向TxManager发起创建事务组操作

    事务发起方也就是客户端,在我们的前面的test过程中,就是meep-test服务

    TxManager就是上篇提到的JtaTransactionManager类

    创建事务组指向上篇提到的doBegin方法,调用路径如下

    JdkDynamicAopProxy.doJtaBegin->UserTransaction.doBegin->transactionManager.begin,最后的begin代码如下

    doBegin

    再看一下JTA深度历险的begin流程

    doBegin

    对比后,我们可以看到,meepo/byteJta多了一些内容,原因是JTA深度历险中的是JTA规范的原型代码,是必须的要有的步骤,实际实现中还需要考虑更多。

    如上下文transactionContext实例,作用是序列化后放入dubbo或者springcloud其他服务的上下文,这样分支服务可以意识到全局事务的存在;

    设置事务的过期时间,是因为meepo中有定时work对超期的事务进行清理;

    this.associateThread(transaction); 把事务绑定到一个单例的map里,为什么不直接用原型中的ThreadLocal呢?因为事务的故障恢复、异步处理等场景需要按线程检索事务实例,使用map会更方便;

    注:后面的Transaction接口(不清楚再看一下深度历险)上没有定义 begin 方法,原因:Transaction 对象本身就代表了一个事务,在它被创建的时候就表明事务已经开始,因此也就不需要额外定义 begin方法了。

  • 第二步:执行业务

    业务分本地业务和远程业务,byteJta的sample如下

    demo

    无论本地还是远程业务,在JTA规范里,都属于resource,通过enlistResource加入到全局事务管理中

    接下来我们跟踪本地方法 this.increaseAmount(targetAcctId, amount);

    只有一句代码:int value = this.jdbcTemplate.update("update tb_account_two set amount = amount + "+amount+" where acct_id ='"+acctId+"'");调试进入

    jdbcTemplate-update

    update方法里有一个内部类,在最后excute中传入了一个它的实例,显然,这是一种回调做法。这里讲一下jdbcTemplate的回调思想,因为比较经典

    现在假设我们自己写一个类似jdbcTemplate,封装jdbc的模块

    使用过JDBC的同学都知道资源用完之后要马上释放,否则会资源泄漏。数据库连接的资源我们可以在最后进行释放,比较麻烦的是statement和ResultSet的释放,一开始我的设想是写一个类似如下的函数

    错误demo

    但是这样做有很大问题,比如statement和ResultSet都没有释放,ResultSet还可以指望好心的用户调用一下close()方法释放,但是statement呢?于是后来又想到下面这个方法:

    错误demo

    这样做虽然把资源释放了,但是却让代码的耦合度更高了。这个query函数是把结果写入文件,那么假如用户还需要把结果保存到某个对象中该怎么办呢? 那就只好再写一个类似的函数,大部分代码都是重复的,只是while部分的代码会变。虽然也可以把这部分代码抽取出来作为另外一个函数,但是一旦有 新的对ResultSet的操作加入进来,这个类文件势必要更改,明显不满足开闭原则。

    看一下jdbcTemplate的代码

    jdbcTemplate-query

    回调方式改造上面的错误方案

    callback-demop

    在我这个项目中,获取到数据库的查询结果之后需要马上处理(因为要释放资源嘛),但又不知道具体要怎么处理, 这时候也是用回调机制可以很好地解决问题。总结一下就是,如果我写一个API去获取某些数据,获取到之后要把数据给用户去做具体操作, 同时用户操作完之后控制权还要回到我的手中,那么回调机制是很好的解决方案。另外,AOP也是同样的思想,用户自定义在切入点前后需要被 执行的回调函数,然后框架会帮我们去调这些函数,从而实现如事务管理等功能。

    回归正题,继续跟踪update代码,接下来的步骤是获取connection

    jdbcTemplate-update

    根据meepo-test的数据源配置,经过几个步骤后,进入dbcp2的DataSourceXAConnectionFactory类的createConnection方法

    createConnection

    在获取connection和datasource的时候,使用动态代理获得了增强的实现类,作用后文会讲到,继续跟踪getConnection方法

    获取连接

    重点关注下ManagedConnection类,根据注释,这是一个dbcp2连接池获取connection的类,我们将在这里为jdbcTemplate获取一个connection,并且更新事务状态,把connection- enlist到全局事务中

    注释

    简单翻译下,ManagedConnection负责管理事务环境中的数据库连接,当没有全局事务(XA事务或JTA事务)时,运行方式与其他连接类似。当一个 全局事务处于活动状态,所有的单个物理连接与全局事务共享。连接共享意味着所有事务期间的数据访问具有一致的数据库视图。当全局事务  提交或回滚,则单个物理连接相应提交或回滚

    构造方法

    更新事务状态

    更新事务状态

    1、ManagedConnection的transactionContext默认为空,只有经过updateTransactionStatus方法才会赋值, 如果此处transactionContext!=null且是活跃状态,则意味着本地方法有多个sql语句,此时必须保证事务状态没有变化, 因为一个事务内的sql全部执行后才会变成提交或者回滚状态,否则抛出异常;

    TransactionContext类是一个本地事务上下文,翻译下注释 :表示单个XAConnectionFactory与Transaction之间的关联。    此上下文包含一个共享连接,所有ManagedConnections都应该使用该连接XAConnectionFactory侦听事务,当发生完成事件时关闭ManagedConnection的共享连接,并初始化状态

    监听事务

    初始化状态

    2、从transactionManager中获取活跃的事务内容

    TransactionContext

    此处的transactionManager也就是前面提到从xml中注入的bean,可在ManagedConnection的set方法反向追踪;

    获取的transaction也就是 begin方法中初始化的全局事务;

    根据获取的transaction实例化一个TransactionContext对象并放入弱引用map中作为缓存

    3、如果存在共享连接,意味着本地事务体内除第一次外的其他sql执行,不会把当前连接放入共享连接,即在一个本地事务中,只会把一个连接enlist到全局事务中

    4、第一次sql执行,将当前连接加入到全局事务中,注意 transactionContext.setSharedConnection(connection);这一句

    enlist-resource

    enlist代码和深度历险的实现相同,最后放入transaction的list中,并执行本地XAResource(如MysqlXAConnection) 的start 和 end 方法

    enlist完成后,得到connection,并创建Statement,执行sql,和jdbc的一致。这里的 connection和Statement都是通过动态代理增强的实现,具体实现和作用请见后文。

    本地sql执行完成后,因为在事务的管理中,sql结果并不会生效,如果打开mysql的general_log观察,此时已经有sql记录,但相应表数据并未改变

到这里,我们就解析完了整个本地sql执行的流程,下文会对远程服务对执行进行跟踪,如果有任何问题或建议,欢迎在github 上提issue,如果觉得有帮助,打个star鼓励下把!