面试官:MyBatis 的事务是如何管理的?和 Spring 整合后,为什么不再需要手动 commit/rollback?Spring 是如何接管 MyBatis 事务的?
一、开篇
事务管理是持久层框架的核心能力之一。MyBatis 本身提供了轻量级的事务管理机制,但在与 Spring 整合后,事务控制权被透明地转移给了 Spring 的声明式事务管理。
本篇目标:
- 深入 MyBatis 原生事务管理体系(
Transaction接口及实现) - 理解
JdbcTransaction和ManagedTransaction的区别 - 追踪 Spring 整合后事务的接管机制(
SqlSessionTemplate+ 事务同步器) - 解析
@Transactional下为什么不需要手动 commit/rollback - 搞清楚 Spring 与 MyBatis 事务同步的底层原理
二、MyBatis 原生事务管理体系
2.1 Transaction 接口
// org.apache.ibatis.transaction.Transactionpublic interface Transaction {Connection getConnection() throws SQLException;voidcommit() throws SQLException;voidrollback() throws SQLException;voidclose() throws SQLException;Integer getTimeout() throws SQLException;}
三、JdbcTransaction — 原生 JDBC 事务
JdbcTransaction 是 MyBatis 默认的事务实现,直接使用 JDBC 的 Connection 进行事务控制。
public class JdbcTransaction implements Transaction {private static final Log log = LogFactory.getLog(JdbcTransaction.class);protected Connection connection;protected DataSource dataSource;protected TransactionIsolationLevel level;protected boolean autoCommit;protected boolean skipSetAutoCommitOnClose; // ⭐ 控制关闭时是否重置 autoCommit// 构造函数1:从 DataSource 创建public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {this(ds, desiredLevel, desiredAutoCommit, false);}// 构造函数2:从 DataSource 创建,可指定 skipSetAutoCommitOnClosepublic JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel,boolean desiredAutoCommit, boolean skipSetAutoCommitOnClose) {dataSource = ds;level = desiredLevel;autoCommit = desiredAutoCommit;this.skipSetAutoCommitOnClose = skipSetAutoCommitOnClose;}// 构造函数3:从已有 Connection 创建(如 Spring 整合时传入已绑定的连接)public JdbcTransaction(Connection connection) {this.connection = connection;}@Overridepublic Connection getConnection() throws SQLException {if (connection == null) {openConnection(); // 延迟获取连接}return connection;}@Overridepublic void commit() throws SQLException {// 仅当连接存在且不是自动提交模式时才提交if (connection != null && !connection.getAutoCommit()) {if (log.isDebugEnabled()) {log.debug("Committing JDBC Connection [" + connection + "]");}connection.commit();}}@Overridepublic void rollback() throws SQLException {// 仅当连接存在且不是自动提交模式时才回滚if (connection != null && !connection.getAutoCommit()) {if (log.isDebugEnabled()) {log.debug("Rolling back JDBC Connection [" + connection + "]");}connection.rollback();}}@Overridepublic void close() throws SQLException {if (connection != null) {// ⭐ 关键:关闭前重置 autoCommitresetAutoCommit();if (log.isDebugEnabled()) {log.debug("Closing JDBC Connection [" + connection + "]");}connection.close();}}protected void openConnection() throws SQLException {if (log.isDebugEnabled()) {log.debug("Opening JDBC Connection");}connection = dataSource.getConnection();if (level != null) {connection.setTransactionIsolation(level.getLevel());}// 通过独立方法设置 autoCommit(会检查当前值)setDesiredAutoCommit(autoCommit);}// 设置 autoCommit,先检查当前值是否已经是期望值protected void setDesiredAutoCommit(boolean desiredAutoCommit) {try {if (connection.getAutoCommit() != desiredAutoCommit) {if (log.isDebugEnabled()) {log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");}connection.setAutoCommit(desiredAutoCommit);}} catch (SQLException e) {throw new TransactionException("Error configuring AutoCommit. Your driver may not support getAutoCommit() or setAutoCommit(). "+ "Requested setting: " + desiredAutoCommit + ". Cause: " + e, e);}}// 关闭连接前重置 autoCommit 为 trueprotected void resetAutoCommit() {try {if (!skipSetAutoCommitOnClose && !connection.getAutoCommit()) {// MyBatis 不会在没有更新操作时调用 commit/rollback。// 某些数据库(如 Sybase)在关闭连接前要求必须提交或回滚,// 将 autoCommit 设为 true 可以避免这个问题。if (log.isDebugEnabled()) {log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");}connection.setAutoCommit(true);}} catch (SQLException e) {if (log.isDebugEnabled()) {log.debug("Error resetting autocommit to true before closing the connection. Cause: " + e);}}}@Overridepublic Integer getTimeout() throws SQLException {return null;}}
3.1 关键机制解读
(1)resetAutoCommit()— 关闭前的兼容性处理
某些数据库驱动(如 Sybase、一些老版本 Oracle 驱动)在关闭Connection时,如果当前处于非自动提交模式且事务未提交/回滚,会抛出异常。resetAutoCommit()在关闭连接前将autoCommit重置为true,避免了这个问题。
(2)commit()/rollback()的条件判断
如果连接已经是自动提交模式(autoCommit=true),每条 SQL 执行后都已自动提交,此时commit()不应再做任何操作。这个判断确保 MyBatis 不会在自动提交模式下错误地提交事务。
(3)skipSetAutoCommitOnClose字段
某些场景下(如 Spring 整合),连接关闭由 Spring 管理,MyBatis 不需要干预autoCommit,此时可设为true跳过重置。
(4)三个构造函数
- 两个
DataSource构造函数:用于独立使用 MyBatis 时,通过数据源创建连接,MyBatis 自行管理事务。 - 一个
Connection构造函数:用于 Spring 整合场景,Spring 事务管理器已从数据源获取连接并绑定到当前线程,MyBatis 直接使用该连接。
四、ManagedTransaction — 容器管理事务
ManagedTransaction 将事务的 commit() 和 rollback() 完全委托给容器(如 Spring 或 Java EE 应用服务器),自身不执行任何提交/回滚操作。
// org.apache.ibatis.transaction.managed.ManagedTransactionpublic class ManagedTransaction implements Transaction {private static final Log log = LogFactory.getLog(ManagedTransaction.class);private DataSource dataSource;private TransactionIsolationLevel level;private Connection connection;private final boolean closeConnection; // ⭐ final,构造后不可变// 构造函数1:从已有 Connection 创建public ManagedTransaction(Connection connection, boolean closeConnection) {this.connection = connection;this.closeConnection = closeConnection;}// 构造函数2:从 DataSource 创建public ManagedTransaction(DataSource ds, TransactionIsolationLevel level, boolean closeConnection) {this.dataSource = ds;this.level = level;this.closeConnection = closeConnection;}@Overridepublic Connection getConnection() throws SQLException {if (this.connection == null) {openConnection(); // 延迟获取}return this.connection;}@Overridepublic void commit() throws SQLException {// Does nothing — 事务由容器管理}@Overridepublic void rollback() throws SQLException {// Does nothing — 事务由容器管理}@Overridepublic void close() throws SQLException {if (this.closeConnection && this.connection != null) {if (log.isDebugEnabled()) {log.debug("Closing JDBC Connection [" + this.connection + "]");}this.connection.close();}}protected void openConnection() throws SQLException {if (log.isDebugEnabled()) {log.debug("Opening JDBC Connection");}this.connection = this.dataSource.getConnection();if (this.level != null) {this.connection.setTransactionIsolation(this.level.getLevel());}}@Overridepublic Integer getTimeout() throws SQLException {return null;}}
4.1 关键机制解读
(1)commit()/rollback()为空方法
事务控制权完全交给容器,MyBatis 不做任何干预。这是ManagedTransaction与JdbcTransaction最本质的区别。
(2)closeConnection控制连接关闭
true(默认):close()时关闭连接,适用于 MyBatis 独立使用。false:close()时不关闭连接,适用于连接由容器管理且容器负责关闭的场景。
(3)两个构造函数
ManagedTransaction(Connection connection, boolean closeConnection):直接传入已有连接,适用于 Spring 整合时从TransactionSynchronizationManager获取当前线程绑定的连接。ManagedTransaction(DataSource ds, TransactionIsolationLevel level, boolean closeConnection):通过 DataSource 获取连接,适用于独立使用场景。
(4)延迟连接获取
getConnection()首次调用时才通过openConnection()获取,避免提前占用数据库连接资源。
4.2 Spring 整合中的实际使用
在 Spring 整合环境中,虽然mybatis-config.xml中通常配置transactionManager="JDBC",但 Spring 的SqlSessionFactoryBean会创建自定义Environment,实际使用的是SpringManagedTransactionFactory。它生成的SpringManagedTransaction实现了Transaction接口。
SpringManagedTransaction 的行为是有条件的:
- 如果 Spring 事务处于活动状态,其
commit()/rollback()方法为空实现(no-op),事务完全由 Spring 的PlatformTransactionManager(如DataSourceTransactionManager)管理。 - 如果 没有 Spring 活动事务,它的行为与
JdbcTransaction类似,会实际调用Connection的commit()/rollback()方法。
// SpringManagedTransaction(mybatis-spring 3.x)public class SpringManagedTransaction implements Transaction {private final DataSource dataSource;private Connection connection;private boolean isConnectionTransactional;@Overridepublic Connection getConnection() throws SQLException {if (this.connection == null) {// 方法稍微整理了一下,未与源码严格一致// 从 Spring 事务同步器中获取当前线程绑定的连接this.connection = DataSourceUtils.getConnection(this.dataSource);this.isConnectionTransactional =DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);}return this.connection;}@Overridepublic void commit() throws SQLException {// 不做任何事情,由 Spring 事务管理器控制}@Overridepublic void rollback() throws SQLException {// 不做任何事情,由 Spring 事务管理器控制}}
五、事务与 Executor 的绑定
回顾第 4 篇中 openSessionFromDataSource 的流程:
// DefaultSqlSessionFactoryprivate SqlSession openSessionFromDataSource(ExecutorType execType,TransactionIsolationLevel level,boolean autoCommit) {Transaction tx = null;try {final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory =getTransactionFactoryFromEnvironment(environment);// 1. 创建 Transactiontx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 2. 创建 Executor 并传入 Transactionfinal Executor executor = configuration.newExecutor(tx, execType);// 3. 创建 SqlSessionreturn new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx);throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);}}
Executor持有Transaction引用,所有数据库操作都通过Transaction获取连接、提交、回滚。
事务与 SqlSession 的绑定:
- 一个
SqlSession对应一个Executor,对应一个Transaction - 事务边界 =
SqlSession生命周期(从openSession到close)
六、Spring 整合后的事务接管
6.1 核心变革:SqlSessionTemplate
在 Spring 整合环境中,你不再直接使用 DefaultSqlSession,而是通过 SqlSessionTemplate。它内部持有 SqlSessionFactory,并通过 JDK 动态代理控制每次操作的行为。
// org.mybatis.spring.SqlSessionTemplatepublic class SqlSessionTemplate implements SqlSession, DisposableBean {private final SqlSessionFactory sqlSessionFactory;private final ExecutorType executorType;//动态代理private final SqlSession sqlSessionProxy;private final PersistenceExceptionTranslator exceptionTranslator;public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory,ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {Assert.notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");Assert.notNull(executorType, "Property 'executorType' is required");this.sqlSessionFactory = sqlSessionFactory;this.executorType = executorType;this.exceptionTranslator = exceptionTranslator;// 创建动态代理this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(SqlSessionFactory.class.getClassLoader(),new Class[]{SqlSession.class},new SqlSessionInterceptor());}}
关键:所有方法调用都会经过 SqlSessionInterceptor,由它决定从 Spring 事务同步器中获取还是新建 SqlSession。
6.2 SqlSessionInterceptor 核心逻辑
// org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptorprivate class SqlSessionInterceptor implements InvocationHandler {// 私有构造,限制只能在当前外部类中实例化private SqlSessionInterceptor() {}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 1. 从 Spring 事务同步器中获取 SqlSession(若不存在则新建)SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,SqlSessionTemplate.this.executorType,SqlSessionTemplate.this.exceptionTranslator);Object unwrapped; // 最终返回的结果或异常try {// 2. 执行目标方法(调用真实的 SqlSession)Object result = method.invoke(sqlSession, args);// 3. 如果该 SqlSession 不在 Spring 事务管理下,立即提交if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {sqlSession.commit(true);}unwrapped = result;} catch (Throwable t) {// 4. 异常处理(核心!)unwrapped = ExceptionUtil.unwrapThrowable(t);// 如果配置了 exceptionTranslator 且异常是 PersistenceExceptionif (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {// ⭐ 先关闭 SqlSession(释放连接),再转换异常SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);sqlSession = null; // 标记已关闭,防止 finally 中重复关闭Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);if (translated != null) {unwrapped = translated; // 替换成 Spring 的 DataAccessException}}// 重新抛出异常(可能是原始异常,也可能是转换后的)throw (Throwable) unwrapped;} finally {// 5. 如果 SqlSession 未被关闭(即没有发生异常转换),在 finally 中关闭if (sqlSession != null) {SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);}}}}
6.3getSqlSession核心逻辑
SqlSessionUtils.getSqlSession 负责从 Spring 的事务同步器中获取或创建 SqlSession:
// org.mybatis.spring.SqlSessionUtilspublic static SqlSession getSqlSession(SqlSessionFactory sessionFactory,ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {Assert.notNull(sessionFactory, "No SqlSessionFactory specified");Assert.notNull(executorType, "No ExecutorType specified");// 1. 从当前线程获取 SqlSessionHolderSqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);// 2. 通过 sessionHolder 方法检查是否可复用SqlSession session = sessionHolder(executorType, holder);if (session != null) {return session;}// 3. 创建新的 SqlSessionLOGGER.debug(() -> "Creating a new SqlSession");session = sessionFactory.openSession(executorType);// 4. 注册到事务同步器registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);return session;}
6.4 事务同步管理器:TransactionSynchronizationManager
Spring 通过TransactionSynchronizationManager维护当前线程的事务资源,包括:
- 当前线程绑定的
DataSource连接 - 事务的隔离级别、传播行为等
- 事务同步回调(如
SqlSessionSynchronization)
TransactionSynchronizationManager内部使用ThreadLocal存储这些信息,保证了线程隔离。
关键流程:
@Transactional方法开启时,Spring 开启事务,从 DataSource 获取连接,绑定到ThreadLocal。- 首次执行 Mapper 操作时,
SqlSessionInterceptor通过getSqlSession获取SqlSessionHolder(新建并绑定)。 - 后续同一事务中的 Mapper 操作,直接从
ThreadLocal中获取同一个SqlSession。 - 事务提交时,
SqlSessionSynchronization回调触发SqlSession.commit(),连接随之提交。 - 事务回滚时,触发
SqlSession.rollback(),然后关闭连接。
七、Spring 整合后为什么不需要手动 commit/rollback
在没有 Spring 事务的情况下(如方法未加 @Transactional),SqlSessionInterceptor 会为每次 Mapper 方法调用创建一个新的 SqlSession,并在方法执行完毕后立即提交并关闭该 SqlSession。这保证了每个数据库操作都是独立、自动提交的,但其行为与原生 MyBatis 的 autoCommit=true 模式有所不同——这里是 SqlSessionTemplate 在每次方法调用级别管理了 SqlSession 的生命周期。
SqlSessionInterceptor中的判断:
if (!isSqlSessionTransactional(sqlSession, sqlSessionFactory)) {// 非事务托管:手动提交sqlSession.commit(true);}// 事务托管:不提交,交给 Spring
- 当处于
@Transactional方法内时,isSqlSessionTransactional返回true,MyBatis 不会提交,由 Spring 管理。 - 当没有事务时,
isSqlSessionTransactional返回false,MyBatis 在每个操作后自动提交。
八、Spring 整合中的事务传播行为
8.1 传播行为对 SqlSession 的影响
核心原则:SqlSession 是否复用,取决于当前线程是否已经绑定了一个与 SqlSessionFactory 关联的 SqlSessionHolder。SqlSessionUtils.getSqlSession() 方法会优先从 TransactionSynchronizationManager 中获取已绑定的 SqlSession。
不同的事务传播行为会影响事务的创建和挂起,从而间接影响SqlSession的获取:
传播行为 | 对 SqlSession 的影响 |
REQUIRED | 若当前有事务,复用同一个 |
REQUIRES_NEW | 挂起当前事务(解绑 |
NESTED | 在现有事务中通过保存点(Savepoint) 实现嵌套。 |
SUPPORTS | 若当前有事务则参与并复用 |
NOT_SUPPORTED | 挂起当前事务,以非事务方式执行。当前线程的 |
MANDATORY | 必须存在事务,复用当前事务绑定的 |
NEVER | 必须在非事务环境下执行。若当前有事务则抛异常,且不会复用 |
8.2 REQUIRES_NEW 示例
@Servicepublic class OuterService {@Transactionalpublic void outerMethod() {userMapper.selectById(1); // 使用 SqlSession1innerService.innerMethod(); // 调用内层}}@Servicepublic class InnerService {@Transactional(propagation = Propagation.REQUIRES_NEW)public void innerMethod() {orderMapper.selectById(1); // 使用 SqlSession2(新事务)}}
执行结果:
- 外层事务中创建并绑定
SqlSession1 - 内层
REQUIRES_NEW挂起外层事务,创建SqlSession2 - 内层提交时只提交
SqlSession2,外层的事务独立
九、面试高频题(附源码级回答)
Q1:MyBatis 原生的JdbcTransaction和ManagedTransaction有什么区别?
A:
JdbcTransaction:MyBatis 原生事务实现,直接调用Connection.commit()/rollback(),适合独立使用。close()前会调用resetAutoCommit()将autoCommit重置为true,避免某些数据库驱动报错。ManagedTransaction:commit()/rollback()为空方法,事务由容器管理。closeConnection字段控制close()时是否关闭连接。
Q2:JdbcTransaction中resetAutoCommit()的作用是什么?
A:某些数据库驱动(如 Sybase)在关闭Connection时,如果当前处于非自动提交模式且事务未提交/回滚,会抛出异常。resetAutoCommit()在关闭连接前将autoCommit重置为true,避免这个问题。skipSetAutoCommitOnClose可控制是否跳过此操作,适用于 Spring 等容器管理连接的场景。
Q3:Spring 整合后,SqlSession是如何与当前线程绑定的?
A:通过 SqlSessionUtils.getSqlSession() 方法,使用 TransactionSynchronizationManager.getResource(sessionFactory) 从当前线程中获取已绑定的 SqlSessionHolder。SqlSessionHolder 是对 SqlSession 的包装,内部维护了引用计数,用于管理 SqlSession 的打开与关闭生命周期。如果当前有事务且 SqlSessionHolder 存在,直接复用其中的 SqlSession;否则创建一个新的 SqlSession 并包装为 SqlSessionHolder 绑定到 TransactionSynchronizationManager 中。Spring 事务结束时,通过注册的 SqlSessionSynchronization 回调自动关闭 SqlSession 并解绑。
Q4:@Transactional方法中调用多个 Mapper,会创建多少个SqlSession?
A:只会创建一个SqlSession。首次调用任何 Mapper 时,SqlSessionInterceptor触发SqlSessionUtils.getSqlSession()创建并绑定到当前线程。后续 Mapper 调用复用同一个SqlSession,确保所有数据库操作在同一个事务上下文和数据库连接中执行。
Q5:sqlSession.commit(true)中的force参数是什么意思?
A:force 参数用于强制提交,即使当前 SqlSession 没有执行过更新操作(dirty 为 false)。在 Spring 非事务环境下,SqlSessionInterceptor 在每个数据库操作后都会调用 sqlSession.commit(true),这是 MyBatis-Spring 的设计约定——非事务模式下每个操作独立提交,force=true 确保提交流程能够正常完成,即使当前会话没有任何数据变更。
Q6:Spring 整合后,一级缓存还有效吗?
A:有效。Spring 事务内复用同一个SqlSession,因此一级缓存(SqlSession级别)在事务内是共享的。不同事务之间由于SqlSession不同,一级缓存隔离。事务方法内多次查询同一个 key 能命中一级缓存。
Q7:为什么在 Spring 环境中使用SqlSessionTemplate而不直接用DefaultSqlSession?
A:DefaultSqlSession 是非线程安全的——它本身设计为绑定单个数据库会话,不应被多线程共享。且它无法感知 Spring 的事务上下文。SqlSessionTemplate 是线程安全的,内部通过 ThreadLocal 和动态代理机制,确保每个线程/事务使用正确的 SqlSession 实例。同时,SqlSessionTemplate 还能自动将 MyBatis 的异常转换为 Spring 的 DataAccessException。
十、下篇预告
第 12 篇(最终篇)将进行全系列总结 + 面试高频 30 题 + 常见坑点汇总。
如果觉得有帮助,欢迎 点赞、在看、转发 支持!
夜雨聆风