首先,说明一下,这里指的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加强方法对象,用户,线程
我大致表示了一下各个元素之间的数量关系,用户访问的过程大概是这样:
4、
上面的分析中,我们看到从保证可行性的角度上
只要做到了以上两点,用AOP实现事务管理就是逻辑上可行的,具体实现我们可以先不管,毕竟现在只是猜测,后面再去看我们猜的对不对。
而且,我们还能大胆猜测,保存Connection使用了ThreadLocal,ThreadLocal可以简单地看成一个Map<线程ID,保存对象>,可以保存线程本地变量。
我们现在的目标就是在源码中找到这些对象
现在,我们要正式开始探索源码,这是我们的思路:
1、首先,我们得先找到源码在哪,如果我们采用的纯xml配置,可以说,事务管理是完全不会注入到我们的代码中,这是特别好的一点,但是对我们学习的来说,就比较麻烦了,只能从配置出发,有两个重要的信息
2、这是我们的两个出发点,由于我们还完全不了解源码,直接看第一条还是比较累的,我们从AOP这一点出发,用调试来帮我们
给我们的业务类方法一个断点,并调用
然后看一下方法栈
很容易就可以找到实现事务管理的MethodInterceptor,基本也就知道它是用CGLib实现的,这个类就是TransactionInterceptor
3、找到了入口,我们就给invoke方法打上断点开始不断调试,大致了解整个过程,不用深入,只是简单地过一遍,看下运行结果即可,我只讲一些重要的点,其他就看我的方法栈吧
以上的方法栈是这个阶段为止的,如果看不清可以去看下我ProcessOn的Spring源码方法栈,其他重要的知识点也有,多少对Spring源码学习也有帮助,大家看着乐乐就好了,因为有些东西可能是有错的,我胆子比较大,直接把我的猜测写上去的,错了可能没有改掉。
4、我们看下它大致的流程:
5、然后让我们开始分析这个方法栈中有哪些重要的点:
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
这块东西,一定要自己去看源码,我不大会讲,也没有篇幅去给大家一一看源码的方法栈,所以一定要自己看。
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、正式分析源码之前,我们肯定得先清楚下事务的隔离级别和传播机制,这块我给一个链接。
首先,有一件事情需要大家注意一下:
隔离级别:
3. ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,A事务可以看到B事务未提交的数据
这种隔离级别会产生脏读,不可重复读和幻像读。
4. ISOLATION_READ_COMMITTED:A事务可以读取B事务修改后提交的数据,但是不能读取B事务未提交的数据,不再读取未提交的数据,但是没有行锁,修改有效,少了脏读
5. ISOLATION_REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。原因是开了行锁,禁止了事务开启期间修改内容,不影响继续新增行,所以还是会幻读。这是Mysql默认的
6. ISOLATION_SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。
事务的传播机制:
上面的东西我都是抄的,要么就是稍微加点自己的理解,原作者是大炮的大炮没有大炮,原文就是上面那个链接。
3、接着看之前的代码
此为完整的方法栈,总体来说后面的部分比较简单,也不用完整得看,但是你看我的图肯定是没有用的,还是要自己过一遍的。
我们几乎完全了解了Spring的事务管理过程,虽然细节关注的不是很深,但是我们还是完全可以知道它做了些什么。还有一个比较让我意外的是,原来事务的传播机制是Spring来做的,一直以为是数据库提供的实现,我们默认用的是REQUIRED,所以我也没有太多看其他的。
补缺1、
Service方法之间的互相调用问题:
这里的互相调用分成两种:
解答:
这与其说是事务管理的问题,更多像是AOP的原理问题。
AOP实现原理是递归调用所有加强方法,最后调用被代理对象的方法
补缺2、
但是1中被调用的方法会在结束后的加强方法中提交,而外部方法也会在结束后的加强方法中提交,这其中是否会出错,需要我们再关注一下,我不是很清楚为什么现在很少人问这个问题,而是去问调用this的方法不生效的问题
我多次调试之后发现,它执行提交事务有一个条件就是它的tansactionStatus的newTransaction必须为true,这是我疏漏的一点
然后我们基本可以猜到它仅在第一次取得时newTransaction会为true,而从TransactionSynchronization取得的话就是false,并且通过TransactionAspectSupport来保存新老transactionStatus,来记住谁是newTransaction。
阅读源码肯定得回头看几遍的,不然谁还记得false是干什么的,它表示从TransactionSynchronization取出来的就设置为false,或者说默认就是false,也就代表它不会被提交。
而在doBegin,也就是新建时就会设置为true
这样我们就解决了不会重复提交的问题。
补缺3、
。。。大家认为还有的话就提一下,我会加
我大致把Spring的事务管理理清了,总得来说,一开始我一直不敢动,弄完之后发现还挺简单的,这一块内容还有很多,还有不少是分布式事务管理的,我是被这些吓坏了。。。
所以说看源码还是得理清目标,找准突破口,不然看都不敢看。这是我看源码的思路,感觉还是挺有帮助的,大家共勉吧。