
Spring注解事务的实现
mybatis-spring包为事务提供的支持
动态数据源使用配置需要注意的问题
动态数据源配置例子
我遇到过的就这两点:
同一个bean中调用自身的添加事务注解的方法
使用动态数据源配置不正确导致的
一个Service方法中直接调用另一个被声明事务的方法,因为是在this中调用的,就走不到事务的切面方法,也就直接导致事务不生效,对于此类问题,可以通过ApplicationContext获取Bean再调用,不要直接使用this调用。

关于第二点,使用动态数据源配置不正确导致的事务不起作用问题,我将留在文末分析,因为只有了解Spring事务的工作原理,才能真正的理解为什么会出现这样的问题。
TransactionInterceptor是事务方法拦截器,或叫切面。TransactionInterceptor继承TransactionAspectSupport。本篇不分析Spring AOP部分的实现,只关注事务的实现。
当调用一个bean的被@Transaction注解注释的方法时,先走到TransactionInterceptor事务拦截器的invoke方法,因此事务拦截器的invoke方法就是分析注解事务实现的入口。
由于invoke方法调用invokeWithinTransaction,自身并不做任何事情,所以直接看invokeWithinTransaction方法。
【TransactionInterceptor#invokeWithinTransaction方法】

invokeWithinTransaction方法整体分为四块
1、初始化事务支持,根据注解的属性配置,处理事务传播机制、为事务创建连接、为连接设置事务隔离级别等;
spring-tx:
TransactionInterceptor(事务方法拦截器、切面)
PlatformTransactionManager(平台事务管理器)
TransactionStatus(事务状态)
ConnectionHolder(连接持有者)
TransactionSynchronizationManager(事务同步管理者)

【一些关键对象的类图】

首先获取注解的属性配置信息,如事务隔离级别、是否只读事务、事务的传播机制、事务超时时间等(TransactionAttribute)。
接着获取事务管理器PlatformTransactionManager,如果@Transaction注解上指定了事务管理器则获取指定的事务管理器,否则使用默认的。一般我们只会注册一个事务管理器。除非配置了多数据源(非动态数据源),才会为每个数据源配置一个事务管理器。
初始化工作由createTransactionIfNecessary方法完成。

调用createTransactionIfNecessary方法创建事务(如果需要),IfNecessary说明并不一定会创建,比如当前已经存在一个事务,则根据事务的传播机制决定是否要创建。createTransactionIfNecessary方法返回一个TransactionInfo,TransactionInfo保存事务信息,如旧的事务的TransactionInfo、事务属性配置、事务管理器、当前事务方法的事务状态。
protected final class TransactionInfo {// 事务管理器@Nullableprivate final PlatformTransactionManager transactionManager;// 事务注解的属性配置@Nullableprivate final TransactionAttribute transactionAttribute;// 切入点标志private final String joinpointIdentification;// 事务状态@Nullableprivate TransactionStatus transactionStatus;// 前一个事务的事务信息@Nullableprivate 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 JdbcTransactionObjectSupportimplements SavepointManager, SmartTransactionObject {// 持有数据库连接@Nullableprivate ConnectionHolder connectionHolder;// 之前的事务隔离级别,用于当事务退出时,还原Connection的事务隔离级别@Nullableprivate Integer previousIsolationLevel;// 是否允许使用保存点private boolean savepointAllowed = false;}
【DataSourceTransactionObject】
private static class DataSourceTransactionObjectextends JdbcTransactionObjectSupport {// 持有的ConnectionHolder是否是新创建的private boolean newConnectionHolder;// 事务结束时是否需要重置连接为自动提交private boolean mustRestoreAutoCommit;}
【ConnectionHolder】
public class ConnectionHolder extends ResourceHolderSupport {@Nullableprivate ConnectionHandle connectionHandle;// 当前数据库连接@Nullableprivate Connection currentConnection;// 当前事务状态private boolean transactionActive = false;// 是否支持保存点@Nullableprivate Boolean savepointsSupported;// 当前连接的事务的保存点总数private int savepointCounter = 0;}
1、调用doGetTransaction创建事务对象DataSourceTransactionObject。它继承JdbcTransactionObjectSupport实现的接口是SavepointManager,为当前事务提供创建保存点、回退到保存点、释放保存点继续执行的支持,而具体的创建保存点这些操作会交给ConnectionHolder完成的。DataSourceTransactionObject还会保存之前的事务隔离级别,用于当前事务退出时,还原Connection的事务隔离级别,否则当连接放入连接池被复用时就可能出现问题。

2、调用isExistingTransaction方法判断当前是否已经有事务存在了,如果当前存在事务,则调用handleExistingTransaction方法返回一个TransactionStatus,根据配置的事务传播机制处理。
// 判断当前是否已经存在事务,其实是判断DataSourceTransactionObject// 是否已经从TransactionSynchronizationManager// 拿到了一个ConnectionHolder,且ConnectionHolder的transactionActive状态是否为true@Overrideprotected boolean isExistingTransaction(Object transaction) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());}
这里会涉及到几种传播机制的处理,此处我只介绍其中的一种传播机制的处理。PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
【handleExistingTransaction方法部分代码】



3、如果不走步骤2,则当前不存在事务。为当前方法创建一个TransactionStatus,并调用doBean方法。doBean方法由子类DataSourceTransactionManager实现。

doBean方法中判断当前DataSourceTransactionObject是否持有ConnectionHandler,如果有,则说明从TransactionSynchronizationManager中能拿到ConnectionHolder,已经存在一个事务了。如果没有,则从数据源中获取一个连接,并创建ConnectionHolder,赋值给DataSourceTransactionObject,并标志这是一个新创建的连接。
获取到连接之后,需要为当前事务对象,设置事务信息,分以下几步解析:






TransactionSynchronizationManager的resources静态字段类型为ThreadLocal,所以同一个线程上的事务方法都能获取到同一个连接。用一个Map存储线程数据。如果是存储ConnectionHodler的,则Key为数据源DataSouce(如果使用动态数据源则为动态数据源)。后面分析SqlSession时,也是用这个字段存储的,所以Key定义为Object类型。

完成事务的初始化工作之后,接着就是执行目标方法
retVal = invocation.proceedWithInvocation();
如果方法执行出现异常,则执行回滚逻辑。

事务管理器的rollback方法分析:

否则如果方法执行正常,则执行提交逻辑。

从事务信息TransactionInfo中获取事务状态TransactionStatus,调用事务管理器的commit方法完成提交。

mybatis-spring-boot-autoconfigure包下的MybatisAutoConfiguration 会自动完成一些配置工作,如创建SqlSessionTemplate这个bean。
源码分析涉及到的一些类说明
mybatis-spring:
SqlSessionTemplate(SqlSession的代理+委托)
SqlSessionInterceptor(SqlSession代理拦截器)
SpringManagedTransaction(mybatis-spring实现的mybatis的Transaction)
SpringManagedTransactionFactory(mybatis-spring实现的mybatis的TransactionFactory)

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(// SqlSessionTemplateSqlSession 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。
当key的类型为数据源时,存储的就是当前事务的连接持有者ConnectionHodler;
当key的类型为SqlSession工厂时,存储的就是当前SqlSession持有者SqlSessionHodler;
可能还存在其它的,只是当前我发现的就这两种。所以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配置*/@Beanpublic 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*/@Beanpublic 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*/@Beanpublic 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@ConditionalOnMissingBeanpublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}// SqlSessionTemplate也是由此自动创建的@Bean@ConditionalOnMissingBeanpublic SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {}....}
试一试,配置一个动态数据源,给动态数据源分配两个都是指向同一个数据库的数据源,然后配置SqlSessionFactoryBean(SqlSessionFactory)使用一个非动态数据源,而给PlatformTransactionManager事务管理器使用这个动态数据源,写一个事务例子,看看会发生什么?






