Spring事务管理的实现原理

Spring事务管理的实现原理

背景

首先,说明一下,这里指的Spring事务管理指的是spring-tx包下,仅考虑非分布式事务管理的部分。我还没学过分布式事务管理,就是一个比较有耐心肯看源码的小白罢了。

我们使用SSM框架编写自己的项目的时候,对事务管理的处理往往都停留在配置这一层,完全不去考虑它是如何实现的。现在,有了些经验之后,我就想着应该有能力去研究它的实现原理了,事务管理对数据库连接的操作的思想绝对是值得学习的。

还有一点就是,这篇博客,是我边学边写的,多少会有我逐步分析的逻辑,如果能看到这篇博客,那我基本就ok了。。。

正文

这块的学习方式,我大致就是先分析,再猜测,然后通过调试和阅读源码来证实我的猜测,并做出调整。

第一轮学习:总体分析,目标确认

总体分析

首先是最初的分析,我们得去认识一下Transaction(事务)这个东西有哪些东西需要我们注意。

1、
我们使用事务的目的肯定是为了保证原子性,其重点就是开启事务,提交,回滚和设置savepoint,在Mybatis中,Transaction类就是对Connection的封装,通过调用Connection以下方法来实现

void commit() throws SQLException;
void rollback() throws SQLException;
Savepoint setSavepoint(String name) throws SQLException;
void rollback(Savepoint savepoint) throws SQLException;
void releaseSavepoint(Savepoint savepoint) throws SQLException;

所以我们考虑的关键还是Connection对象,只要持有了Connection对象,我们就可以实现事务管理的操作。


2、
我们的事务管理一般是业务级的,也就是会对Service层的某个方法作为一个完整的事务,调用前开启事务,调用后提交,调用中途有异常就会回滚,这里就不考虑复杂的savepoint。
看到这段分析,第一瞬间就想到了AOP,通过加强方法来给所有Service业务方法提供事务支持,那么是不是让代理对象的handler持有connection就行了呢?


3、
显然是不行的,首先,service是单例的,对于并发的web项目,至少它应该是一个能制造Connection的DataSource才行。不过,就算解决了这个问题,我们需要连接的Dao层也取不到Connection对象。现在,还是让我们好好分析下各个元素之间的关系再去讨论谁来持有Connection。

先列出我们要讨论的元素:

数据库,数据源,Connection,Transaction,后端项目,Service对象,Dao对象,Handler加强方法对象,用户,线程

在这里插入图片描述
我大致表示了一下各个元素之间的数量关系,用户访问的过程大概是这样:

  1. 多个用户访问一个后端项目,后端创建多个Thread分别处理每个请求
  2. 每个Thread调用同一个Service对象的方法进入业务方法,这时候Handler就需要取得一个Connection,并且得想办法保存下来,交给Dao层对象
  3. DataSource用来取得Connection,一个DataSource可以生成多个Connection,一个数据源需要url和用户密码来匹配一个数据库及账户,理论上一个数据库有多个数据源,但在非分布式项目中,而且一个事务只能绑定一个数据源,就当成只有一个数据源
  4. Dao层调用Connection的方法(Mybatis就是做了这个)操作数据库,并返回给Service
  5. Service层根据调用过程在结束后完成事务管理
  6. 为用户返回结果

4、
上面的分析中,我们看到从保证可行性的角度上

  1. Handler必须直接或间接引用DataSource来生成Connection,同时防止Handler越权,最好找一个对象去引用DataSource
  2. 还要一个对象去保存每个线程的Connection对象,用来提供给需要Connection的Dao层对象

只要做到了以上两点,用AOP实现事务管理就是逻辑上可行的,具体实现我们可以先不管,毕竟现在只是猜测,后面再去看我们猜的对不对。
而且,我们还能大胆猜测,保存Connection使用了ThreadLocal,ThreadLocal可以简单地看成一个Map<线程ID,保存对象>,可以保存线程本地变量。

我们现在的目标就是在源码中找到这些对象


第二轮学习:正式进入源码,寻找突破口

第二次分析

现在,我们要正式开始探索源码,这是我们的思路:

1、首先,我们得先找到源码在哪,如果我们采用的纯xml配置,可以说,事务管理是完全不会注入到我们的代码中,这是特别好的一点,但是对我们学习的来说,就比较麻烦了,只能从配置出发,有两个重要的信息

  1. 我们可以得到DataSourceTransactionManager这个类
  2. 它是用AOP实现的

2、这是我们的两个出发点,由于我们还完全不了解源码,直接看第一条还是比较累的,我们从AOP这一点出发,用调试来帮我们

给我们的业务类方法一个断点,并调用
在这里插入图片描述
然后看一下方法栈
在这里插入图片描述
很容易就可以找到实现事务管理的MethodInterceptor,基本也就知道它是用CGLib实现的,这个类就是TransactionInterceptor

3、找到了入口,我们就给invoke方法打上断点开始不断调试,大致了解整个过程,不用深入,只是简单地过一遍,看下运行结果即可,我只讲一些重要的点,其他就看我的方法栈吧

在这里插入图片描述
以上的方法栈是这个阶段为止的,如果看不清可以去看下我ProcessOn的Spring源码方法栈,其他重要的知识点也有,多少对Spring源码学习也有帮助,大家看着乐乐就好了,因为有些东西可能是有错的,我胆子比较大,直接把我的猜测写上去的,错了可能没有改掉。

4、我们看下它大致的流程:

  1. 读取事务的属性
  2. 确定事务管理器(其实就是我们配置的)
  3. 开启事务,并取得事务
  4. 调用业务方法
  5. 再用事务对象来commit

5、然后让我们开始分析这个方法栈中有哪些重要的点:

  1. 读取了我们配置的事务管理属性,对应的就是getTransactionAttributeSource(),如果用xml配置得到的就是我们配置的属性,如果是注解配置,返回的就是注解的解析器,其他的我就没有细看,总的来说,注解和xml配置只能选其一
  2. 找到了事务管理核心的类,TransactionAspectSupport,也就是TransactionInterceptor的父类
  3. 找到了我们配置的事务管理器
  4. 事务管理的几个重要数据类
    1. TransactionDefinition,它只是记录了事务的隔离等级和传播机制,我们不是特别关心
    2. TransactionAttribute,它继承自上面那个,多了一个限定词和判断异常是否回滚的方法(就是我们配的)
    3. TransactionStatus,它封装了Connection对象,是我们关注的重点
    4. TransactionInfo,一堆信息的总和,包括前三个,事务管理器,被管理的业务方法名等
  5. 我们一直想要找到的存储了Connection的类,不过它不是对象存储,而是类静态存储,这个我们下面来和事务管理器一起细讲

5、事务开启的起点就是调用事务管理器的getTransaction(),这次我只看它前段代码,如何取出Connection,如何创建,是我们下一步关心的事情

// AbstractPlatformTransactionManager.java
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException {
    
    
		// Use defaults if no transaction definition given.
		TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());
		Object transaction = doGetTransaction();
	...
}
// DataSourceTransactionManager.java
protected Object doGetTransaction() {
    
    
		DataSourceTransactionObject txObject = new DataSourceTransactionObject();
		txObject.setSavepointAllowed(isNestedTransactionAllowed());
		ConnectionHolder conHolder =
				(ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource());
		txObject.setConnectionHolder(conHolder, false);
		return txObject;
}

其中的ConnectionHolder就是我们的重点,它持有了Connection对象
然后我们就得认识一下TransactionSynchronizationManager这个保存了Connection对象的类了

public abstract class TransactionSynchronizationManager {
    
    
	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");

	public static Object getResource(Object key) {
    
    
		Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
		Object value = doGetResource(actualKey);
		return value;
	}

	private static Object doGetResource(Object actualKey) {
    
    
		Map<Object, Object> map = resources.get();
		if (map == null) {
    
    
			return null;
		}
		Object value = map.get(actualKey);
		return value;
	}
}

它印证了我们之前的猜想,它用了ThreadLocal,而且以DataSource为键,存储了Connection对象,resources大概是个Map<Thread, Map<DataSource, Connection>>,而且它全部采用了静态,这样全局所有对象都可以取到Connection了,包括我们的Dao层对象。

这块东西,一定要自己去看源码,我不大会讲,也没有篇幅去给大家一一看源码的方法栈,所以一定要自己看。

6、然后我们去Mybatis中看看它是如何取用Connection对象的,我可以保证它无论自己写了多少代码,但一定得使用这个TransactionSynchronizationManager

入口肯定是我们配置的SqlSessionFactoryBean,然后可以看到它创建的还是DefaultSqlSessionFactory,但是它生产SqlSession的方法

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    
    
    Transaction tx = null;
    try {
    
    
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
    
    
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
    
    
      ErrorContext.instance().reset();
    }
  }

创建了TransactionFactory,通过调试可以知道,它的实现类是SpringManagedTransactionManager,虽然它依旧是Mybatis自己写的,但是我们继续往下找,看到SpringManagedTransaction中getConnection方法中,使用了openConnection(),它有一行代码是

this.connection = DataSourceUtils.getConnection(this.dataSource);

而DataSourceUtils是springframe包下的类了,在它的doGetConnection(datasource)方法中找到了

ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

我们的猜想得到了完全的印证

总结

我们首先猜测有一个对象会保存Connection,并且会使用到ThreadLocal,这从Spring事务管理到Mybatis取用Connection都得到了应证,可以说是大成功,不过第一个猜测是会保存DataSource确实有点不一样,我没想到它竟然会以DataSource为键,不过这样也确实方便取用和创建Connection,接下来,我们就要去看它是如何创建Connection和事务了。

第三轮学习:研究后续事务开启源码,完成整个过程

第三次分析

1、现在,我们的方向明确,继续分析AbstractPlatformTransactionManager的getTransaction方法,之前的只是取得Connection,但是我们现在还没有创建Connection,后续还要把创建的Connection保存到TransactionSynchronizationManager中。然后就是比较简单的调用事务代码和提交或者回滚事务。

2、正式分析源码之前,我们肯定得先清楚下事务的隔离级别和传播机制,这块我给一个链接。

首先,有一件事情需要大家注意一下:

  1. 事务的隔离是由Mysql等数据库实现的,你也可以设置
  2. 事务的传播机制是Spring定义的,其功能的实现也是Spring完成的,Mysql我怎么查都查不到,我这边可能有点错误,大佬看了可以指正一下

隔离级别:
3. ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,A事务可以看到B事务未提交的数据
这种隔离级别会产生脏读,不可重复读和幻像读。
4. ISOLATION_READ_COMMITTED:A事务可以读取B事务修改后提交的数据,但是不能读取B事务未提交的数据,不再读取未提交的数据,但是没有行锁,修改有效,少了脏读
5. ISOLATION_REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。原因是开了行锁,禁止了事务开启期间修改内容,不影响继续新增行,所以还是会幻读。这是Mysql默认的
6. ISOLATION_SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。

事务的传播机制:

  1. PROPAGATION_REQUIRED:当前没事务就开启,有就加入
  2. PROPAGATION_SUPPORTS:当前有事务就加入,没有就不开事务
  3. PROPAGATION_MANDATORY:当前有事务就加入,没有就报错
  4. PROPAGATION_REQUIRES_NEW :当前有事务就挂起该事务,先执行此事务后再执行之前的事务
  5. PROPAGATION_NOT_SUPPORTED:当前有事务就挂起,反正自己不开事务
  6. PROPAGATION_NEVER:当前有事务就报错,自己不会开事务
  7. PROPAGATION_NESTED:当前有事务,通过savepoint产生嵌套事务

上面的东西我都是抄的,要么就是稍微加点自己的理解,原作者是大炮的大炮没有大炮,原文就是上面那个链接。

3、接着看之前的代码

此为完整的方法栈,总体来说后面的部分比较简单,也不用完整得看,但是你看我的图肯定是没有用的,还是要自己过一遍的。
在这里插入图片描述

总结

我们几乎完全了解了Spring的事务管理过程,虽然细节关注的不是很深,但是我们还是完全可以知道它做了些什么。还有一个比较让我意外的是,原来事务的传播机制是Spring来做的,一直以为是数据库提供的实现,我们默认用的是REQUIRED,所以我也没有太多看其他的。

第四轮分析:根据实际应用,查漏补缺

第四次分析

补缺1、
Service方法之间的互相调用问题:
这里的互相调用分成两种:

  • 调用其他service对象的方法
  • 调用this的方法

解答:
这与其说是事务管理的问题,更多像是AOP的原理问题。
AOP实现原理是递归调用所有加强方法,最后调用被代理对象的方法

  1. 被代理对象被注入的其他service对象是代理对象,调用的时候肯定是会开启事务,但是我们用的是REQUIED,所以会加入事务,没有影响
  2. 如果是调用this的方法,此时的this为被代理对象,所以调用的是未被加强的方法,需要谨慎使用

补缺2、
但是1中被调用的方法会在结束后的加强方法中提交,而外部方法也会在结束后的加强方法中提交,这其中是否会出错,需要我们再关注一下,我不是很清楚为什么现在很少人问这个问题,而是去问调用this的方法不生效的问题

我多次调试之后发现,它执行提交事务有一个条件就是它的tansactionStatus的newTransaction必须为true,这是我疏漏的一点
在这里插入图片描述
然后我们基本可以猜到它仅在第一次取得时newTransaction会为true,而从TransactionSynchronization取得的话就是false,并且通过TransactionAspectSupport来保存新老transactionStatus,来记住谁是newTransaction。

在这里插入图片描述
阅读源码肯定得回头看几遍的,不然谁还记得false是干什么的,它表示从TransactionSynchronization取出来的就设置为false,或者说默认就是false,也就代表它不会被提交。

在这里插入图片描述
而在doBegin,也就是新建时就会设置为true
这样我们就解决了不会重复提交的问题。

补缺3、
。。。大家认为还有的话就提一下,我会加

总结

我大致把Spring的事务管理理清了,总得来说,一开始我一直不敢动,弄完之后发现还挺简单的,这一块内容还有很多,还有不少是分布式事务管理的,我是被这些吓坏了。。。
所以说看源码还是得理清目标,找准突破口,不然看都不敢看。这是我看源码的思路,感觉还是挺有帮助的,大家共勉吧。

猜你喜欢

转载自blog.csdn.net/weixin_44815852/article/details/113808099