MYBATIS-SPRING · SQLSESSIONTEMPLATE · TRANSACTIONSYNCHRONIZATIONMANAGER
Spring 开的事务,MyBatis 的 SQL 怎么进去的
@Transactional 归 Spring 管,SQL 却是 MyBatis 发的。两边能对上,靠的是同一个线程上的一个“储物柜”:Spring 把数据库连接放进去,MyBatis 从里面拿。
Spring 这边
开事务:拿一个连接,关掉自动提交,放进柜子。结束时由它提交或回滚。
MyBatis 这边
DataSourceTransactionManager开事务时拿连接、放进柜子;真正的提交和回滚都是它做。
SpringTransactionSynchronizationManager几个 ThreadLocal。一个线程一个柜子,按 key 存东西。
SpringSqlSessionTemplate每次 Mapper 调用都经过它。柜子里有会话就复用,没有就新开并登记。
mybatis-springSpringManagedTransactionMyBatis 要连接,它去柜子里拿 Spring 的;MyBatis 要提交,它说交给 Spring。
mybatis-spring启动时:两边接到同一个 DataSource 上
柜子里的连接是按 DataSource 这个 key 存的。事务经理和 MyBatis 用的必须是同一个 DataSource 对象,才能存取到同一格。
DataSourceTransactionManagerBoot 自动配置好(也可能是它的子类 JdbcTransactionManager)SqlSessionFactoryBean没指定 transactionFactory 时,默认用 SpringManagedTransactionFactory@Bean
SqlSessionFactory sqlSessionFactory(DataSource ds) throws Exception {
SqlSessionFactoryBean f = new SqlSessionFactoryBean();
f.setDataSource(ds); // 和事务经理用同一个
// 不设 transactionFactory,默认就是 SpringManagedTransactionFactory
return f.getObject();
}
@Bean
PlatformTransactionManager txManager(DataSource ds) {
return new DataSourceTransactionManager(ds); // 同一个 DataSource
}
@Bean
SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory f) {
return new SqlSessionTemplate(f); // 你的 Mapper 都通过它干活
}
- 用 Spring Boot 时,这三个 Bean 都由自动配置给你造好(Spring 第 03 篇):MyBatis 的 starter 造前一个和最后一个,Spring 的 JDBC 自动配置造事务经理。
@MapperScan登记的每个 Mapper,底下用的都是这个 SqlSessionTemplate。- JdbcTemplate 取连接也走 DataSourceUtils,所以和 MyBatis 混用时,同一个事务里拿到的是同一个连接。
柜子里放了什么
储物柜就是 TransactionSynchronizationManager 里的几个 ThreadLocal。事务里最要紧的是这三格。
连接
- key
DataSource- 放的是
ConnectionHolder,里面是连接 #A- 谁放的
- 事务经理,开事务时
- 谁来拿
- MyBatis、JdbcTemplate,都经过 DataSourceUtils
- 何时清走
- 事务提交或回滚之后
MyBatis 会话
- key
SqlSessionFactory- 放的是
SqlSessionHolder,里面是会话 S1- 谁放的
- SqlSessionTemplate,事务里第一次调 Mapper 时
- 谁来拿
- 同一事务里,后面每一次 Mapper 调用
- 何时清走
- 提交或回滚之前
收尾清单
- 形式
- 一个列表,不按 key 存
- 放的是
SqlSessionSynchronization- 提交前
- 叫 S1 commit:清一级缓存、刷出攒着的批量语句
- 结束前
- 把 S1 从柜子里拿走并关掉
- 挂起时
- 把 S1 挪出柜子,恢复时再放回来
跟着一个下单方法走一遍
选一个剧情,一步一步看:代码走到哪一行,柜子里有什么,数据库里哪些改动已经生效,日志打出了什么。
代码
储物柜
数据库里的改动
doBegin三段关键源码
都是简化版,去掉了日志和边角判断,留下决定行为的那几行。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession session = getSqlSession(factory, executorType, translator); // 拿会话,见 ②
try {
Object result = method.invoke(session, args); // 真正执行 SQL
if (!isSqlSessionTransactional(session, factory)) {
session.commit(true); // 不在事务里:自己提交
}
return result;
} catch (Throwable t) {
throw translate(t); // MyBatis 异常 → Spring 的 DataAccessException
} finally {
closeSqlSession(session, factory); // 在事务里:只是还回柜子;不在:真的关掉
}
}
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(factory);
if (holder != null && holder.isSynchronizedWithTransaction()) {
return holder.getSqlSession(); // 柜子里有:复用
}
SqlSession session = factory.openSession(executorType); // 没有:新开一个
if (TransactionSynchronizationManager.isSynchronizationActive()) { // 正在 Spring 事务里
holder = new SqlSessionHolder(session, executorType, translator);
TransactionSynchronizationManager.bindResource(factory, holder); // 放进柜子
TransactionSynchronizationManager.registerSynchronization(
new SqlSessionSynchronization(holder, factory)); // 登记收尾
}
return session;
public Connection getConnection() throws SQLException {
if (connection == null) {
connection = DataSourceUtils.getConnection(dataSource); // 柜子里有 Spring 的连接,就拿它
autoCommit = connection.getAutoCommit();
isConnectionTransactional = DataSourceUtils.isConnectionTransactional(connection, dataSource);
}
return connection;
}
public void commit() throws SQLException {
if (connection != null && !isConnectionTransactional && !autoCommit) {
connection.commit(); // 只有连接不归 Spring 管时,才自己提交
}
}
它也走 DataSourceUtils,拿到的是同一个 #A,所以在同一个事务里。
事务里会话复用,第二次查才能命中。MyBatis 第 01 篇的剧情演示过。
比如唯一键冲突变成 DuplicateKeyException。都是 RuntimeException,默认会触发回滚。
看日志就知道有没有进事务
配上 logging.level.org.mybatis.spring=debug 和 logging.level.org.springframework.jdbc=debug,每次 Mapper 调用都会说明自己的处境。
在事务里
不在事务里
以为加了事务却看到右边这几行,说明这次调用根本没进事务。常见原因是自调用、方法不是 public、另开了线程,见下面的坑。
JDK 8 和 JDK 17:机制一样,包名和版本不同
柜子、会话复用、谁来提交,这一套在两个版本里完全相同。
Spring 5 · Boot 2
JDK 8+- mybatis-spring 2.x(starter 2.3.x)
- 除了 Spring 自己的
@Transactional,也认javax.transaction.Transactional - CGLIB 代理下,
@Transactional只对 public 方法生效
Spring 6 · Boot 3
JDK 17+- mybatis-spring 3.x(starter 3.0.x),要求 Spring 6
- JTA 注解换成
jakarta.transaction.Transactional;还写 javax 版的,不报错,也不开事务 - 6.0 起,CGLIB 代理下 protected 和包可见的方法上的
@Transactional也生效
常踩的坑
先看日志里那几句,再对照柜子想:这次调用时,柜子里有没有东西。
事务压根没开:同一个类里自己调自己、方法不是 public、异常被 catch 吞了、抛的是受检异常。原因都在 Spring 第 02 篇。
那个线程的柜子是空的,用的是另一个自动提交的连接。要么放回当前线程做,要么让它自己开事务,并接受两边不一致的可能。
事务经理按它自己那个 DataSource 存连接,另一个库的 Mapper 拿到的是别的连接。用 @Transactional("reportTxManager") 指定事务经理;真要跨库一起成败,得上分布式事务。
连接从开事务一直占到提交。事务里调远程接口、发消息、做大计算,连接池很快被占满。把慢操作挪到事务外面。
批量模式先攒着,到提交前(收尾清单通知会话时)才真正发出去,所以拿不到影响行数。要结果就在事务里手动调用 flushStatements()。
同一个事务里,先用普通的 SqlSessionTemplate,又用了 BATCH 的。柜子里那个会话类型已经定了,不能换。把批量操作放进单独的事务。
两个原因叠在一起:会话复用,一级缓存命中了;MySQL 默认的可重复读隔离级别,本来就看不到。
Spring 6 只认 jakarta 版。换成 jakarta.transaction.Transactional,或者直接用 Spring 自己的 @Transactional。