这篇文章写完后我改了N次,反复的读,反复的改,为的是能让读者看懂。源码分析类的文章真的很难写,懂是一回事,写又是另一回事,能写给别人看得懂又是一回事。太多英文单词搞得排版有点乱。
本篇内容包括:
事务不起作用原因有哪些?
我遇到过的就这两点:
一个Service方法中直接调用另一个被声明事务的方法,因为是在this中调用的,就走不到事务的切面方法,也就直接导致事务不生效,对于此类问题,可以通过ApplicationContext获取Bean再调用,不要直接使用this调用。
关于第二点,使用动态数据源配置不正确导致的事务不起作用问题,我将留在文末分析,因为只有了解Spring事务的工作原理,才能真正的理解为什么会出现这样的问题。
Spring注解事务的实现
TransactionInterceptor是事务方法拦截器,或叫切面。TransactionInterceptor继承TransactionAspectSupport。本篇不分析Spring AOP部分的实现,只关注事务的实现。
当调用一个bean的被@Transaction注解注释的方法时,先走到TransactionInterceptor事务拦截器的invoke方法,因此事务拦截器的invoke方法就是分析注解事务实现的入口。
由于invoke方法调用invokeWithinTransaction,自身并不做任何事情,所以直接看invokeWithinTransaction方法。
【TransactionInterceptor#invokeWithinTransaction方法】
invokeWithinTransaction方法整体分为四块
1、初始化事务支持,根据注解的属性配置,处理事务传播机制、为事务创建连接、为连接设置事务隔离级别等;
2、调用目标方法;
3、方法执行异常完成事务回滚;
4、方法执行成功完成事务提交。
源码分析涉及到的一些类说明:
spring-tx:
【一些关键对象的类图】
01
首先获取注解的属性配置信息,如事务隔离级别、是否只读事务、事务的传播机制、事务超时时间等(TransactionAttribute)。
接着获取事务管理器PlatformTransactionManager,如果@Transaction注解上指定了事务管理器则获取指定的事务管理器,否则使用默认的。一般我们只会注册一个事务管理器。除非配置了多数据源(非动态数据源),才会为每个数据源配置一个事务管理器。
初始化工作由createTransactionIfNecessary方法完成。
调用createTransactionIfNecessary方法创建事务(如果需要),IfNecessary说明并不一定会创建,比如当前已经存在一个事务,则根据事务的传播机制决定是否要创建。createTransactionIfNecessary方法返回一个TransactionInfo,TransactionInfo保存事务信息,如旧的事务的TransactionInfo、事务属性配置、事务管理器、当前事务方法的事务状态。
protected final class TransactionInfo {
// 事务管理器
@Nullable
private final PlatformTransactionManager transactionManager;
// 事务注解的属性配置
@Nullable
private final TransactionAttribute transactionAttribute;
// 切入点标志
private final String joinpointIdentification;
// 事务状态
@Nullable
private TransactionStatus transactionStatus;
// 前一个事务的事务信息
@Nullable
private TransactionInfo oldTransactionInfo;
}
调用平台事务管理器PlatformTransactionManager的getTransaction方法为当前事务方法创建一个TransactionStatus,TransactionStatus描述一个事务的状态,如该事务是否存在保存点、是否已完成等。
public interface TransactionStatus extends SavepointManager {
// 是否是新创建的事务
boolean isNewTransaction();
// 是否存在保存点
boolean hasSavepoint();
void setRollbackOnly();
// 是否只回滚
boolean isRollbackOnly();
// 事务是否为已完成,即已提交或已回滚。
boolean isCompleted();
}
接着分析PlatformTransactionManager的getTransaction方法,其实就是分析DataSourceTransactionManager的getTransaction方法。
在分析方法之前,先认识两个对象DataSourceTransactionObject与ConnectionHolder:
【DataSourceTransactionObject继承JdbcTransactionObjectSupport】
public abstract class JdbcTransactionObjectSupport
implements SavepointManager, SmartTransactionObject {
// 持有数据库连接
@Nullable
private ConnectionHolder connectionHolder;
// 之前的事务隔离级别,用于当事务退出时,还原Connection的事务隔离级别
@Nullable
private Integer previousIsolationLevel;
// 是否允许使用保存点
private boolean savepointAllowed = false;
}
【DataSourceTransactionObject】
private static class DataSourceTransactionObject
extends JdbcTransactionObjectSupport {
// 持有的ConnectionHolder是否是新创建的
private boolean newConnectionHolder;
// 事务结束时是否需要重置连接为自动提交
private boolean mustRestoreAutoCommit;
}
【ConnectionHolder】
public class ConnectionHolder extends ResourceHolderSupport {
@Nullable
private ConnectionHandle connectionHandle;
// 当前数据库连接
@Nullable
private Connection currentConnection;
// 当前事务状态
private boolean transactionActive = false;
// 是否支持保存点
@Nullable
private Boolean savepointsSupported;
// 当前连接的事务的保存点总数
private int savepointCounter = 0;
}
1、调用doGetTransaction创建事务对象DataSourceTransactionObject。它继承JdbcTransactionObjectSupport实现的接口是SavepointManager,为当前事务提供创建保存点、回退到保存点、释放保存点继续执行的支持,而具体的创建保存点这些操作会交给ConnectionHolder完成的。DataSourceTransactionObject还会保存之前的事务隔离级别,用于当前事务退出时,还原Connection的事务隔离级别,否则当连接放入连接池被复用时就可能出现问题。
如果当前已经存在一个事务,则根据自己持有的数据源从TransactionSynchronizationManager中能拿到ConnectionHolder,如果当前未存在事务,则TransactionSynchronizationManager的getResource返回Null。如果拿到ConnectionHolder,则设置给DataSourceTransactionObject对象,并标志这不是一个新的连接。
2、调用isExistingTransaction方法判断当前是否已经有事务存在了,如果当前存在事务,则调用handleExistingTransaction方法返回一个TransactionStatus,根据配置的事务传播机制处理。
// 判断当前是否已经存在事务,其实是判断DataSourceTransactionObject
// 是否已经从TransactionSynchronizationManager
// 拿到了一个ConnectionHolder,且ConnectionHolder的transactionActive状态是否为true
@Override
protected boolean isExistingTransaction(Object transaction) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
}
这里会涉及到几种传播机制的处理,此处我只介绍其中的一种传播机制的处理。PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
【handleExistingTransaction方法部分代码】
case 1
创建TransactionStatus,为当前连接创建保存点Savapoint。当方法执行过程中发生异常时,从TransactionStatus拿到保存点,回滚到保存点;当执行成功则移除保存点,回到上一个事务方法继续执行。
case 2
走正常的事务逻辑。
3、如果不走步骤2,则当前不存在事务。为当前方法创建一个TransactionStatus,并调用doBean方法。doBean方法由子类DataSourceTransactionManager实现。
doBean方法中判断当前DataSourceTransactionObject是否持有ConnectionHandler,如果有,则说明从TransactionSynchronizationManager中能拿到ConnectionHolder,已经存在一个事务了。如果没有,则从数据源中获取一个连接,并创建ConnectionHolder,赋值给DataSourceTransactionObject,并标志这是一个新创建的连接。
获取到连接之后,需要为当前事务对象,设置事务信息,分以下几步解析:
step 1
调用DataSourceUtils的prepareConnectionForTransaction方法为连接设置事务。如果当前事务属性配置为只读事务,则设置当前连接只能执行读操作;为获取到的连接设置数据库事务隔离级别。保存原来的隔离级别到事务对象DataSourceTransactionObject中,对于事务结束时恢复Connection的事务隔离级别。
step 2
如果当前连接是自动提交的,取消连接的自动提交,否则事务不生效。并且也要保存是否需要在事务结束时,将Connection的auto commit恢复为true。
step 3
调用自身的prepareTransactionalConnection方法,判断是否只读事务,如果是则执行一条sql为当前连接设置事务为只读事务。
step 4
设置当前事务对象持有的ConnectionHolder的事务状态为Active,标志Connection事务已经开启。
step 5
如果需要,为Connection设置事务超时时间。事务注解上的超时属性,如果是-1则不设置。
step 6
判断当前事务对象持有的ConnectionHolder是否是新创建的,如果是,则说明当前线程的方法调用栈上并未有任何方法开启事务,将ConnectionHolder绑定到TransactionSynchronizationManager的resources上,下次有事务进来就可能拿到这个ConnectionHolder。
TransactionSynchronizationManager的resources静态字段类型为ThreadLocal,所以同一个线程上的事务方法都能获取到同一个连接。用一个Map存储线程数据。如果是存储ConnectionHodler的,则Key为数据源DataSouce(如果使用动态数据源则为动态数据源)。后面分析SqlSession时,也是用这个字段存储的,所以Key定义为Object类型。
02
完成事务的初始化工作之后,接着就是执行目标方法
retVal = invocation.proceedWithInvocation();
03
如果方法执行出现异常,则执行回滚逻辑。
1)、从TracsactionInfo中拿到事务注解的属性配置,判断@Transaction注解是否指定当遇到某种异常时才回滚,如果指定了,先判断当前异常类型是否匹配,如果不匹配,则走正常提交逻辑。
2)、否则从事务信息TransactionInfo中获取事务状态TransactionStatus,调用事务管理器的rollback方法完成回滚。
事务管理器的rollback方法分析:
1)、根据事务状态TransactionStatus,判断当前事务是否有保存点Savepoint,如果有,则回滚到保存点,然后释放保存点。
2)、否则如果是个新事务则整个事务回滚(要判断是否是新事务,因为事务的传播机制)。
04
否则如果方法执行正常,则执行提交逻辑。
从事务信息TransactionInfo中获取事务状态TransactionStatus,调用事务管理器的commit方法完成提交。
方法执行分析:
1)、如果当前TracsactionStatus有保存点,则释放保存点;事务还不能提交,因为前一个事务方法被挂起了,还没有执行完成。
2)、否则如果是个新事务,提交事务。
mybatis-spring为事务提供的支持
mybatis-spring-boot-autoconfigure包下的MybatisAutoConfiguration 会自动完成一些配置工作,如创建SqlSessionTemplate这个bean。
源码分析涉及到的一些类说明
mybatis-spring:
SqlSessionTemplate这是一个神奇的SqlSession,即有委托又有代理。SqlSessionTemplate也是实现SqlSession接口的,支持SqlSession的所有方法,同时它又不会去执行,而是交给一个SqlSession代理对象去执行,这个代理对象是在SqlSessionTemplate的构造方法中创建的,使用jdk动态代理。而InvocationHandler正是SqlSessionInterceptor。
public class SqlSessionTemplate implements SqlSession,
DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
// 执行器类型,通过解析Mapper标签的<insert>、<update>、<delete>、<select>标签获得
private final ExecutorType executorType;
// SqlSession代理类,jdk动态代理创建
private final SqlSession sqlSessionProxy;
private final PersistenceExceptionTranslator exceptionTranslator;
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {
this(sqlSessionFactory, executorType,
new MyBatisExceptionTranslator(
sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
}
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 创建代理类
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
}
Mapper接口中的每个方法都会通过解析对应Mapper配置文件的标签生成一个MapperMethod,解析完所有方法之后,会为每个Mapper接口都通过JDK动态代理生成一个MapperProxy对象。
public MapperProxy(
// SqlSessionTemplate
SqlSession sqlSession,
// Mapper接口
Class<T> mapperInterface,
// 反射获取的Method与MapperMethod映射
Map<Method, MapperMethod> methodCache){
}
当调用Mapper接口的方法时,都会进入MapperProxy的invoke方法.
public Object invoke(Object proxy, Method method, Object[] args){
}
通过method获取对应的MapperMethod,调用execute方法执行。
【org.apache.ibatis.binding.MapperMethod#execute】
public Object execute(SqlSession sqlSession, Object[] args){
}
MapperMethod的execute方法通过命令类型(insert、update、delete、select)调用SqlSession的insert、update、delete、selectList、selectOne方法。而此时的SqlSession正是单例的SqlSessionTemplate。
调用SqlSessionTemplate的方法会进入SqlSessionInterceptor的invoke方法(JDK动态代理,SqlSessionInterceptor是InvocationHandler)。此时才会真正的去获取一个SqlSession。
getSqlSession方法的执行步骤分析:
1、从TransactionSynchronizationManager中获取当前是否持有一个SqlSessionHolder,如果有则判断这个SqlSessionHolder的ExecutorType(CRUD)是否相同,相同才使用这个SqlSession。否则调用SqlSessionFactory的openSession方法获取一个SqlSession。
这个TransactionSynchronizationManager前面分析事务的时候已经很熟悉了,它用于存储当前线程数据,而且都是使用resources这个静态字段存储的,这是一个ThreadLocal。
可能还存在其它的,只是当前我发现的就这两种。所以resources的命名由此而来,也正是resource静态字段配置ThreadLocal的泛型类型为Map的原因。
为什么不使用多个ThreadLocal存储的?如果用多个ThreadLocal存储,容易由于疏忽忘了调用哪个ThreadLocal的remove导致内存泄露问题。如果resources不仅仅只是保持ConnectionHodler、SqlSession呢,使用多个ThreadLocal难以管理,最重要的就是代码难以阅读,会显得很乱。这是我的个人观察,看源码就是多吸收一些优秀的编程思想。
SqlSession的创建在SqlSessionFactory的openSessionFromDataSource方法中完成的。
先调用SpringManagedTransactionFactory对象的newTransaction创建一个SpringManagedTransaction(mybatis的Transaction接口的实现类,用于Spring与Mybatis整合提供事务的支持),再创建Executor,最后创建DefaultSqlSession。
// 三者的关系
DefaultSqlSession-> 调用Executor执行方法 -> 调用SpringManagedTransaction获取连接
newTransaction方法传入的数据源是配置SqlSessionFactory时注入的数据源,所以SqlSessionFactory与PlatformTransactionManager配置的数据源要相同,否则事务获取的连接与实际执行Sql获取到的连接将会不同。
2、将创建的SqlSession放到SqlSessionHolder中,将其绑定到TransactionSynchronizationManager(ThreadLocal)。一个事务中,如果执行的命令都是同一种类型,如update,则只会创建一个DefaultSqlSession。如果已经持有一个DefaultSqlSession则会直接使用。
DefaultSqlSession就是mybatis实现的SqlSession,所以往下走就是Mybatis的调用流程。在执行方法过程中,会调用Executor的getConnection方法获取连接,而Executor的getConnection方法会调用Transaction的getConnection方法。
这个Transaction就是SpringManagedTransaction。所以一个SqlSession不等于一个数据库连接。Mybatis中执行SQL之前再通过Transaction获取连接。
SpringManagedTransaction的getConnection方法如果当前已经打开一个连接,则返回当前连接,否则调用DataSourceUtils工具类的getConnection方法获取连接,而getConnection方法传递的参数是当前SpringManagedTransaction对象持有的数据源。根据数据源从TransactionSynchronizationManager中看看当前是否有事务打开了连接,如果有,则从TransactionSynchronizationManager拿到当前事务使用的连接。
如果能从TransactionSynchronizationManager中拿到连接,则说明当前执行的Mapper方法在事务中。如果拿不到则直接从数据源中获取一个连接,也就是走的非事务逻辑了。
动态数据源使用配置需要注意的问题
如果配置的PlatformTransactionManager事务管理器,使用的是一个动态数据源,那么TransactionSynchronizationManager会将这个动态数据源作为Key,因为多个数据源都是被动态数据源管理的,所以动态数据源内部怎么切换,一个事务中TransactionSynchronizationManager持有的都是第一次获取到的连接,整个事务中都会使用这个连接,因此在事务中切换数据源无效,应在事务之前完成数据源的切换。
SqlSessionFactory与PlatformTransactionManager配置的数据源一定要相同!SqlSessionFactory与PlatformTransactionManager配置的数据源一定要相同!SqlSessionFactory与PlatformTransactionManager配置的数据源一定要相同!
TransactionSynchronizationManager通过数据源作为key缓存事务持有的ConnectionHodler,由PlatformTransactionManager创建。SpringManagedTransaction是在DefaultSqlSessionFactory的openSession方法中调用SpringManagedTransactionFactory传入数据源创建的。因此,如果SqlSessionFactory与PlatformTransactionManager配置的数据源不同,ConnectionHodler所用数据源,将会与SpringManagedTransaction使用的数据源不同,ConnectionHodler所持有的连接就不会被Mybatis所使用,事务就不会生效。
动态数据源配置例子
示例一:
@Bean(name = DsConstant.DEFAULT_MYSQL_DB_01)
public DataSource mysqlDatabase01() {
DruidDataSource druidDataSource = new DruidDataSource();
......
return druidDataSource;
}
@Bean(name = DsConstant.DEFAULT_MYSQL_DB_02)
public DataSource mysqlDatabase02() {
DruidDataSource druidDataSource = new DruidDataSource();
......
return druidDataSource;
}
/**
* 动态数据源
*
* @return
*/
@Bean(name = DsConstant.DS)
public DynamicDataSource dynamicDataSource(
@Qualifier(DsConstant.DEFAULT_MYSQL_DB_01) DataSource db1,
@Qualifier(DsConstant.DEFAULT_MYSQL_DB_02) DataSource db2) {
DynamicDataSource ds = new DynamicDataSource();
......
return ds;
}
/**
* SqlSessionFactory配置
*/
@Bean
public SqlSessionFactoryBean platformSqlSessionFactory(@Qualifier(DsConstant.DS) DataSource dynamicDataSource,
@Autowired MybatisProperties mybatisProperties) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// 配置数据源
sqlSessionFactoryBean.setDataSource(dataSource);
PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
// mapper文件位置
sqlSessionFactoryBean.setMapperLocations(mybatisProperties.resolveMapperLocations());
sqlSessionFactoryBean.setConfigLocation(resourcePatternResolver.getResource(mybatisProperties.getConfigLocation()));
// 使用mybatis-spring提供的事务工厂
TransactionFactory transactionFactory = new SpringManagedTransactionFactory();
sqlSessionFactoryBean.setTransactionFactory(transactionFactory);
return sqlSessionFactoryBean;
}
/**
* 事务管理者
*
* @param platformDataSource
* @return
*/
@Bean
public PlatformTransactionManager mysqlPlatformTransactionManager(@Qualifier(DsConstant.DS) DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
示例二:
@Bean(name = DsConstant.DEFAULT_MYSQL_DB_01)
public DataSource mysqlDatabase01() {
DruidDataSource druidDataSource = new DruidDataSource();
......
return druidDataSource;
}
@Bean(name = DsConstant.DEFAULT_MYSQL_DB_02)
public DataSource mysqlDatabase02() {
DruidDataSource druidDataSource = new DruidDataSource();
......
return druidDataSource;
}
/**
* 动态数据源
*
* @Primary注解:声明这个bean为同类型bean的高优先级bean,
* 当自动注入不指定bean的name时,且存在多个同类型的bean时,
* 自动注入会优先使用这个bean
*/
@Primary
@Bean(name = DsConstant.DS)
public DynamicDataSource dynamicDataSource(
@Qualifier(DsConstant.DEFAULT_MYSQL_DB_01) DataSource db1,
@Qualifier(DsConstant.DEFAULT_MYSQL_DB_02) DataSource db2) {
DynamicDataSource ds = new DynamicDataSource();
......
return ds;
}
/**
* 事务管理者
*
* @param platformDataSource
* @return
*/
@Bean
public PlatformTransactionManager mysqlPlatformTransactionManager(@Qualifier(DsConstant.DS) DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
示例二主要借助@Primary注解与mybatis-spring-boot-autoconfigure包下的MybatisAutoConfiguration自动配置类,实现自动配置SqlSessionFactory。
@org.springframework.context.annotation.Configuration
// 条件注入:存在指定的类型才注入这个配置类
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
// 条件注入:容器中存在数据源才注入这个配置类
@ConditionalOnBean(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
// 自动配置在DataSourceAutoConfiguration完成之后
// 如果项目中自己实现配置,则需要导出DataSourceAutoConfiguration这个自动配置
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration {
.....
// 当前未注册SqlSessionFactory,则自动创建SqlSessionFactory
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
}
// SqlSessionTemplate也是由此自动创建的
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
}
....
}
试一试,配置一个动态数据源,给动态数据源分配两个都是指向同一个数据库的数据源,然后配置SqlSessionFactoryBean(SqlSessionFactory)使用一个非动态数据源,而给PlatformTransactionManager事务管理器使用这个动态数据源,写一个事务例子,看看会发生什么?扫码关注最新动态