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 对象到线程池。

相关推荐
IndulgeCui32 分钟前
【金仓数据库产品体验官】KingbaseES-ORACLE兼容版快速体验
数据库
xhbh6661 小时前
【超全汇总】MySQL服务启动命令手册(Linux+Windows+macOS)(上)
数据库·mysql·程序员·mysql启动命令·数据库启动命令
2301_803554523 小时前
mysql(自写)
数据库·mysql
麦麦大数据3 小时前
vue+Django 双推荐算法旅游大数据可视化系统Echarts mysql数据库 带爬虫
数据库·vue.js·django·可视化·推荐算法·百度地图·旅游景点
成都极云科技3 小时前
裸金属服务器与虚拟机、物理机的核心差异是什么?
运维·服务器·数据库
学习中的程序媛~4 小时前
图数据库neo4j的安装
数据库·neo4j
喂完待续4 小时前
【Big Data】AI赋能的ClickHouse 2.0:从JIT编译到LLM查询优化,下一代OLAP引擎进化路径
大数据·数据库·clickhouse·数据分析·olap·big data·序列晋升
柏油4 小时前
MySQL InnoDB 架构
数据库·后端·mysql
一只爱撸猫的程序猿5 小时前
做一个「运维知识库 + 多模态检索问答」的案例
spring boot·aigc·ai编程
10km6 小时前
jsqlparser(六):TablesNamesFinder 深度解析与 SQL 格式化实现
java·数据库·sql·jsqlparser