Spring和mybatis整合后事务拦截器TransactionInterceptor开启提交事务流程

目录

  • 一、说明
  • 二、TransactionInterceptor开启事务
    • (1)、拦截方法
    • (2)、开启事务绑定数据库连接
    • (3)、mybatis中sql执行数据库连接获取
    • (4)、事务提交后,当前线程ThreadLocal清理,sqlSession关闭
  • 三、总结

一、说明

接着上一个博客SpringBoot 声明式事务 源码解析,下面看一下事务开启后把当前数据库连接绑定到ThreadLocal中,mybatis执行数据库操作时,从ThreadLocal中获取连接执行sql,最后拦截器提交或回滚事务,执行sqlsession(一个sqlsession对应一个数据库连接)提交或回滚,然后清理ThreadLocal,关闭sqlsession,数据库连接回收到数据库连接池。

二、TransactionInterceptor开启事务

(1)、拦截方法

java 复制代码
	@Override
	@Nullable
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// Work out the target class: may be {@code null}.
		// The TransactionAttributeSource should be passed the target class
		// as well as the method, which may be from an interface.
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

		// Adapt to TransactionAspectSupport's invokeWithinTransaction...
		return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
	}

执行invokeWithinTransaction方法,如下图,1是获取目标方法上配置的隔离级别和传播属性等属性,2是开启事务的具体方法,3是继续执行后续拦截器最终执行目标方。

下面重点看2方法中的内容。

继续看status = tm.getTransaction(txAttr);

(2)、开启事务绑定数据库连接

下面跟踪方法时,重点看开启事务和绑定数据库连接到TreadLocal中的内容。

在看代码之前,先看看自动配置类中创建了DataSourceTransactionManager组件,不明白什么时候创建的可以看一下我上个博客。组件中放入了dataSource数据源,若依的框架中添加了动态数据源的配置。

下图是status = tm.getTransaction(txAttr);方法中开启事务的内容。doBegin方法时绑定数据库连接到当前线程。doBegin下面prepareSynchronization里面也很重要,记录一下往TransactionSynchronizationManager中设置了许多参数后面会用到,看一下 TransactionSynchronizationManager.initSynchronization();

下图第一个箭头设置了ActualTransctionActive=true在提交事务的时候会用到。往synchronizations放入了空集合,synchronizations是一个ThreadLocal。

下面看一下doBegin方法。

下面是从数据源中获取连接,把连接设置到了txObject的ConnectionHolder数据库连接描述对象中,后续还会从这里面取出。设置了newConnectionHolder=true.

1、先判断连接是不是自动提交,如果是自动提交会设置成不可以自动提交。2、把事务可用状态设置成true,后续会用到。3、把当前连接信息绑定到当前线程。

可以看到resources是ThreadLocal,ThreadLocal中放入了Map集合,key是动态数据源,value是数据库连接描述对象ConnectionHolder。

(3)、mybatis中sql执行数据库连接获取

MybatisAutoConfiguration中会自动注入SqlSessionTemplate组件,@MapperScan中引入了ClassPathMapperScanner组件,组件扫描所有mapper.java文件,把接口设置成MapperFactoryBean类型的组件,可以一下我以前的博客Spring如何管理Mapper,在设置bean的描述时,也设置了SqlSessionTemplate组件到Mapper中,执行到Configuration.addMapper时,knownMappers.put(type, new MapperProxyFactory(type));往map中设置了MapperProxyFactory,当Configuration.getMapper时,会调用MapperProxyFactory生成代理类MapperProxy,默认使用的是jdk动态代理。综上所述,当执行mapper的update方法时,会到MapperProxy代理方法invoke。后面会执行MapperMethod中执行execute方法。

会执行SqlSessionTemplate中的update方法。如下图当SqlSessionTemplate创建的时候,会设置SqlSessionTemplate的代理类sqlSessionProxy,SqlSessionInterceptor是代理类的拦截方法。执行SqlSessionTemplate中的update方法会进入SqlSessionInterceptor的invoke方法。

从if判断可以看到如果没有开启事务,sqlSession会提交事务。如果开启了,使用事务拦截器统一提交事务。

分析一下getSqlSession方法,主要是获取sqlSession,先进入getSqlSession方法看ransactionSynchronizationManager.getResource(sessionFactory);

从resources中获取以sessionFactory为key,值是defaultSqlSession的map,其中resources是ThreadLocal。第一次执行map是null。

请看源码,我添加了注释

java 复制代码
  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
    //从ThreadLocal中获取SqlSessionHolder,可以通过SqlSessionHolder获取生成的sqlSession
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
   //从SqlSessionHolder中获取sqlSession,如果获取都会直接返回
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }
    //通过sessionFactory和执行器类型创建sqlSession
    LOGGER.debug(() -> "Creating a new SqlSession");
    session = sessionFactory.openSession(executorType);
    //把创建好的sqlSession,放入到ThreadLocal中,只有开启事务才能放入
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

看一下openSession代码, tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);

java 复制代码
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
      boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //1、创建了SpringManagedTransaction,传入数据源参数
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //2、创建执行器,传入了SpringManagedTransaction
      final Executor executor = configuration.newExecutor(tx, execType);
      //3、创建DefaultSqlSession,传入了执行器executor 
      return createSqlSession(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();
    }
  }

会创建Transaction类型的组件SpringManagedTransaction,传入了动态数据源。

java 复制代码
 public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    return new SpringManagedTransaction(dataSource);
  }

继续往下跟踪到registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);代码如下

1、里面代码判断synchronizations是不是null,这里上面提到过,在TranscationIntercetor中开启事务后,prepareSynchronization方法中设置了空集合,所以这里是TransactionSynchronizationManager.isSynchronizationActive()=true,synchronizations也是TreadLocal避免了线程不安全问题。

2、创建了SqlSessionHolder其中包含创建好的DefaultSqlSession。

3、往ThreadLocal中放入了Map,map的key是sessionFactory,value是2中创建的SqlSessionHolder。开启事务后,执行后面的sql可以从ThreadLocal取出。

4、创建了SqlSessionSynchronization其中包含创建好的sqlsession,放入到了集合中,此集合会放入1中synchronizations中,后面提交事务的时候会用到。

5、设置了SqlSessionHolder中SynchronizedWithTransaction=true。

总上所述,当开启事务后,在同一个事务中,使用mybatis执行多个sql时,会重复使用同一个DefaultSqlSession,(DefaultSqlSession被绑定到了线程中),也会使用同一个数据库连接,保证可以使用事务。如果没有开启事务,每次执行sql都会重新创建一个DefaultSqlSession。事务的开启和提交回滚都是mybatis来负责的。

继续回到MapperMethod.execute->sqlSession.update(command.getName(), param)

->executor.update(ms, wrapCollection(parameter));

->BaseExecutor.update

->SimpleExecutor.doUpdate

->prepareStatement(handler, ms.getStatementLog());

->Connection connection = getConnection(statementLog);-->openConnection();

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

->doGetConnection 如下代码可以看到从ThreadLocal里面获取map使用动态数据源做key,获取数据库连接描述对象,这个数据库连接对象在开启事务后放入的,可以找找上面的内容有提到。SpringManagedTransaction类中

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

java 复制代码
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
		Assert.notNull(dataSource, "No DataSource specified");

		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
		if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
			conHolder.requested();
			if (!conHolder.hasConnection()) {
				logger.debug("Fetching resumed JDBC Connection from DataSource");
				conHolder.setConnection(fetchConnection(dataSource));
			}
			return conHolder.getConnection();
		}
		// Else we either got no holder or an empty thread-bound holder here.

		logger.debug("Fetching JDBC Connection from DataSource");
		Connection con = fetchConnection(dataSource);

获取数据库连接后会设置到SpringManagedTransaction类中的connection属性中,下面看一下SpringManagedTransaction类关系。


每次创建sqlsession的时候会先都会创建SpringManagedTransaction,SpringManagedTransaction当获取数据库连接后会设置到本类的属性connection上,创建执行器Excutor是会把SpringManagedTransaction设置进去,然后把Excutor设置到sqlsession中。这可以理解为同一个sqlsession对应同一个数据库连接java.sql.Connection。

(4)、事务提交后,当前线程ThreadLocal清理,sqlSession关闭

定位到TransactionAspectSupport.invokeWithinTransaction方法,方法内开启事务,执行拦截器和目标方法最后提交事务。下面看看提交事务方法。

commitTransactionAfterReturning(txInfo);

txInfo.getTransactionManager().commit(txInfo.getTransactionStatus())

->processCommit(defStatus);

->triggerBeforeCompletion

-> TransactionSynchronizationUtils.triggerBeforeCompletion();

遍历TransactionSynchronization执行beforeCompletion

java 复制代码
	for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) {
			try {
				synchronization.beforeCompletion();
			}
			catch (Throwable tsex) {
				logger.error("TransactionSynchronization.beforeCompletion threw exception", tsex);
			}
		}
java 复制代码
  public void beforeCompletion() {
      // Issue #18 Close SqlSession and deregister it now
      // because afterCompletion may be called from a different thread
      if (!this.holder.isOpen()) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Transaction synchronization deregistering SqlSession [" + this.holder.getSqlSession() + "]");
        }
        //删除ThreadLocal中绑定的sessionFactory,和sqlSession
TransactionSynchronizationManager.unbindResource(sessionFactory);
        this.holderActive = false;
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Transaction synchronization closing SqlSession [" + this.holder.getSqlSession() + "]");
        }
        //关闭sqlSession
        this.holder.getSqlSession().close();
      }
    }

主要看一下 TransactionSynchronizationManager.unbindResource(sessionFactory);清理绑定的sessionFactory和sqlSession的map集合

java 复制代码
public static Object unbindResource(Object key) throws IllegalStateException {
		Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
		Object value = doUnbindResource(actualKey);
		if (value == null) {
			throw new IllegalStateException(
					"No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
		}
		return value;
	}
java 复制代码
	private static Object doUnbindResource(Object actualKey) {
		Map<Object, Object> map = resources.get();
		if (map == null) {
			return null;
		}
		Object value = map.remove(actualKey);
		// Remove entire ThreadLocal if empty...
		if (map.isEmpty()) {
			resources.remove();
		}
		// Transparently suppress a ResourceHolder that was marked as void...
		if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
			value = null;
		}
		if (value != null && logger.isTraceEnabled()) {
			logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" +
					Thread.currentThread().getName() + "]");
		}
		return value;
	}

最终提交事务,方法在processCommit->triggerBeforeCommit->TransactionSynchronizationUtils.triggerBeforeCommit(status.isReadOnly())遍历所有的TransactionSynchronization 类型的组件前面添加过SqlSessionSynchronization其中包含了创建好的sqlsession

java 复制代码
public static void triggerBeforeCommit(boolean readOnly) {
		for (TransactionSynchronization synchronization : TransactionSynchronizationManager.getSynchronizations()) {
			synchronization.beforeCommit(readOnly);
		}
	}

获取当前线程的sqlsession,执行提交操作。

执行到sqlsession中执行器的commit,设置不能自动提交。

继续执行执行器中的SpringManagedTransaction中的commit,SpringManagedTransaction在创建sqlsession的时候提到了,

最终获取SpringManagedTransaction中的connection,进行事务提交。

三、总结

1、TransactionInterceptor拦截到目标方法开启事务设置第一个ThreadLocal放入数据源为key,数据库连接描述类为value的map集合。

2、执行mybatis的sql时,sqlSession中的excutor中SpringManagedTransaction类会从第一个ThreadLocal中根据动态数据源取出相应的数据库连接执行sql,保证了开启的事务和执行sql同一个数据库连接。

3、在mybatis的sqlSessionTemplate执行增删改方法时,sqlSession的代理类SqlSession执行getSqlSession,如果开启了事务,出现第二个ThreadLocal,里面存放以sqlSessionFactory为key,defaultSqlSession为value的map集合,如何在同一个事务中,执行每个sql,defaultSqlSession会使用同一个。如果没有开启事务第二个ThreadLocal不生效,每次执行sql都会创建一次defaultSqlSession。事务的开启和提交都是mybatis控制的。
为何开启事务后,在事务中每个mapper增删改查操作都使用同一个sqlsession呢?因为 MyBatis 的 SqlSession 在设计上就是数据库连接(java.sql.Connection)的一个高级封装和门面(Facade)对象。一个 SqlSession 实例在其生命周期内,内部始终持有且仅持有一个 Connection 对象,同一个 SqlSession 就是同一个数据库连接,提交事务时,多个方法使用同一个SqlSession提交方法进而同一个数据库连接提交,保证了事务一致性

一个 SqlSession 实例 → 包含一个 Executor 实例 → 持有一个 Transaction 对象 → 管理一个唯一的 Connection 对象。

4、当事务提交成功或回滚时,会自动清理掉两个ThreadLocal中当前线程中的数据关闭SqlSession,回收 Connection 对象到线程池。

相关推荐
你好潘先生14 小时前
Next.js + Spring Boot 实现 AI 多模型并行对话系统(架构设计与关键实现)
spring boot·向量检索·next.js·pgvector·ai对话·多模型对比·sse流式输出
苍煜14 小时前
SpringBoot单体应用到分布式下的数据库锁、事务、Redis事务、分布式锁、分布式事务协调
数据库·spring boot·分布式
Dylan的码园14 小时前
springBoot与Web后端基础
前端·spring boot·后端
skiy14 小时前
SpringBoot项目中读取resource目录下的文件(六种方法)
spring boot·python·pycharm
xmjd msup14 小时前
mysql的分区表
数据库·mysql
Lyyaoo.14 小时前
【JAVA Spring面经】Spring 事务失效情况
java·数据库·spring
MeAT ITEM14 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
salipopl15 小时前
Spring Boot 整合 Druid 并开启监控
java·spring boot·后端
dovens15 小时前
PostgreSQL 中进行数据导入和导出
大数据·数据库·postgresql
IOT.FIVE.NO.115 小时前
claude code desktop cowork报错解决和记录Workspace..The isolated Linux environment ...
linux·服务器·数据库