MyBatis作为一款广泛应用的持久层框架,其事务管理机制有着独特的设计与实现。今天,咱们就深入剖析MyBatis的事务管理,帮助大家全面掌握其中的核心逻辑。

一、事务基础概念

在深入探讨MyBatis事务管理之前,先简单回顾一下事务的基本概念。事务是数据库操作的一个逻辑单元,由一系列数据库操作组成。它必须满足数据库ACID特性中的一致性要求,即这些操作要么全部成功提交到数据库,要么在出现问题时全部回滚,绝不允许部分操作成功、部分操作失败的情况发生。

举个银行转账的例子,从一个账户扣款和向另一个账户存款这两个操作必须同时成功,才能保证资金的安全以及数据库中金额数据的一致性。如果其中一个操作成功,另一个失败,就会导致数据不一致,出现资金丢失或错误增加的情况。

二、MyBatis事务管理机制

MyBatis主要通过JdbcTransactionManagedTransaction这两种方式来实现事务控制。

(一)JDBC原生事务管理(JdbcTransaction)

JdbcTransaction采用JDBC原生的事务管理方式,借助java.sql.Connection对象来控制事务。在这种模式下,MyBatis从数据源获取Connection对象,之后就需要开发者手动调用Connectioncommit()方法提交事务,调用rollback()方法回滚事务 。下面是JdbcTransaction中提交、回滚和关闭事务的核心源码:

public class JdbcTransaction implements Transaction { // 提交事务 public void commit() throws SQLException { if (connection != null &&!connection.getAutoCommit()) { if (log.isDebugEnabled()) { log.debug("Committing JDBC Connection [" + connection + "]"); } connection.commit(); } } // 回滚事务 public void rollback() throws SQLException { if (connection != null &&!connection.getAutoCommit()) { if (log.isDebugEnabled()) { log.debug("Rolling back JDBC Connection [" + connection + "]"); } connection.rollback(); } } // 关闭事务 public void close() throws SQLException { if (connection != null) { resetAutoCommit(); if (log.isDebugEnabled()) { log.debug("Closing JDBC Connection [" + connection + "]"); } connection.close(); } } // ... 省略其他方法 } 

从代码中可以看到,提交和回滚事务时,会先检查connection是否为空以及自动提交模式是否关闭,如果满足条件,才会执行相应的操作,并在有日志记录需求时打印相关信息。关闭事务时,除了关闭连接,还会重置自动提交模式。

(二)Spring框架的事务管理(ManagedTransaction)

ManagedTransaction意为托管事务,它自身并不管理事务,而是把事务控制工作委托给其他框架。以MyBatis与Spring框架整合的项目为例,通常会借助Spring的事务管理机制来处理事务。在ManagedTransaction类中,提交和回滚事务的方法体为空,具体实现由外部容器负责。

public class ManagedTransaction implements Transaction { // 提交事务 public void commit() throws SQLException { } // 回滚事务 public void rollback() throws SQLException { } // 关闭事务 public void close() throws SQLException { if (connection != null) { resetAutoCommit(); if (log.isDebugEnabled()) { log.debug("Closing JDBC Connection [" + connection + "]"); } connection.close(); } } // ... 省略其他方法 } 

在Spring Boot项目中,引入MyBatis依赖后,无需手动配置ManagedTransaction。因为springboot-start会依据项目配置的数据源,自动创建合适的事务管理器并注册到Spring容器中。开发者直接使用@Transactional注解,就能轻松实现事务控制。

无论是SqlSession还是Executor,它们的事务方法最终都依赖Transaction来完成事务的提交和回滚操作。

三、MyBatis事务的特殊场景

(一)手动事务控制

在MyBatis中,虽然所有SQL执行都由Executor负责,但Executor执行insert()update()等方法时,并不会自动控制事务,即不会出现commitrollback操作。当单纯使用MyBatis框架时,手动控制事务的常见方式如下:

public class ManualTransactionExample { public static void main(String[] args) { try { // 加载MyBatis配置文件 String resource = "mybatis-config.xml"; Reader reader = Resources.getResourceAsReader(resource); SqlSessionFactory sqlSessionFactory = new org.apache.ibatis.session.SqlSessionFactoryBuilder().build(reader); // 手动创建SqlSession,关闭自动提交模式 SqlSession sqlSession = sqlSessionFactory.openSession(false); try { // 执行SQL操作 // 例如调用mapper方法 // UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // userMapper.insertUser(user); // 手动提交事务 sqlSession.commit(); } catch (Exception e) { // 发生异常时回滚事务 sqlSession.rollback(); e.printStackTrace(); } finally { // 关闭SqlSession sqlSession.close(); } } catch (IOException e) { e.printStackTrace(); } } } 

上述代码中,通过sqlSessionFactory.openSession(false)创建SqlSession实例,关闭自动提交模式。执行SQL操作后,若成功则提交事务,若出现异常则回滚事务,最后关闭SqlSession。需要注意,这里的事务控制是手动添加的,并非框架自动处理。在分析Executor中数据操纵方法内部逻辑时,不要默认其包含事务控制操作。

(二)sqlSession生命周期内的多事务情况

JDBC本身没有MyBatis中Session的概念,这就导致在程序中多次执行insertupdate等操作时,会开启多个事务。例如:

// 执行了connection.setAutoCommit(false),并返回 SqlSession sqlSession = MybatisSqlSessionFactory.openSession(); try { StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = new Student(); student.setName("yy"); student.setEmail("email@email.com"); student.setDob(new Date()); student.setPhone(new PhoneNumber("123-2568-8947")); // 第一次插入 studentMapper.insertStudent(student); // 提交 sqlSession.commit(); // 第二次插入 studentMapper.insertStudent(student); // 多次提交 sqlSession.commit(); } catch (Exception e) { // 回滚,只能回滚当前未提交的事务 sqlSession.rollback(); } finally { sqlSession.close(); } 

在这段代码中,正常情况下会开启两个事务:

  • 第一次事务:执行studentMapper.insertStudent(student);时,由于关闭了自动提交,该插入操作被纳入当前事务。调用sqlSession.commit();后,第一次插入操作所在的事务提交,事务结束。
  • 第二次事务:再次执行studentMapper.insertStudent(student);时,开启新事务,该插入操作包含在新事务中。调用sqlSession.commit();后,新事务提交。

如果在执行SQL操作时出现异常,回滚逻辑如下:

  • 第一次insert之后且第一次commit之前发生异常
try { StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = new Student(); // ... 初始化student对象 // 第一次插入 studentMapper.insertStudent(student); // 抛出异常 throw new RuntimeException(); // ... 省略后续插入逻辑 } catch (Exception e) { // 回滚,只能回滚当前未提交的事务 sqlSession.rollback(); } finally { sqlSession.close(); } 

此时rollback会回滚第一次insert操作,因为在第一次commit之前,该操作处于未提交事务中,调用rollback会撤销此事务中的所有操作。

  • 第二次insert之后、第二次commit之前发生异常
try { // 第二次插入 studentMapper.insertStudent(student); // 模拟异常发生 throw new RuntimeException(); // 多次提交 sqlSession.commit(); } catch (Exception e) { // 回滚,只能回滚当前未提交的事务 sqlSession.rollback(); } finally { sqlSession.close(); } 

这种情况下,rollback会回滚第二次insert操作。因为第一次insert操作所在事务已提交,第二次insert操作处于新的未提交事务中,rollback会撤销该未提交事务中的操作,而不会影响第一次提交的内容。

  • 第二次commit之后发生异常
try { StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = new Student(); // ... 初始化student对象 // 第一次插入 studentMapper.insertStudent(student); // 提交 sqlSession.commit(); // 第二次插入 studentMapper.insertStudent(student); // 多次提交 sqlSession.commit(); // 假设这里发生异常 } catch (Exception e) { // 回滚,只能回滚当前未提交的事务 sqlSession.rollback(); } finally { sqlSession.close(); } 

此时rollback不会回滚任何操作,因为两次insert操作所在事务都已提交,不存在未提交事务,调用rollback没有实际效果。

由此可见,当autoCommit=false时,会自动开启事务,执行commit()后事务结束。一个SqlSession生命周期内可以存在多个事务,rollback()只能回滚当前未提交的事务,无法回滚已提交的事务。

(三)关闭自动提交但未执行Commit的情况

以之前的代码为例,若将SqlSessionautoCommit属性设为false,关闭自动提交,且只执行插入操作,未手动调用commit,仅关闭会话,事务内部会进行如下处理:

try { StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); studentMapper.insertStudent(student); } finally { sqlSession.close(); } 

MyBatis在设计时考虑到了这种情况。当执行close()方法时,MyBatis会进行一系列逻辑判断,依据判断结果决定是否执行rollback操作。

// SqlSession # close public void close() { try { // 根据传入的变量判定是否进行回滚操作 executor.close(isCommitOrRollbackRequired(false)); // baseExecutor执行,如果传入true执行回滚操作 dirty = false; } finally { ErrorContext.instance().reset(); } } private boolean isCommitOrRollbackRequired(boolean force) { return (!autoCommit && dirty) || force; } 
// BaseExecutor # close public void close(boolean forceRollback) { try { try { rollback(forceRollback); } finally { if (transaction != null) { transaction.close(); } } // .... 省略无关代码 } } public void rollback(boolean required) throws SQLException { if (!closed) { try { clearLocalCache(); flushStatements(true); } finally { if (required) { //如果为true则执行Transaction中回滚操作 transaction.rollback(); } } } } 

在上述代码中,isCommitOrRollbackRequired方法通过判断autocommitdirty两个关键变量来决定是否回滚。dirty变量用于标识数据是否为脏数据,默认值为false。执行数据更新、插入等操作后,dirty的值会改变,若数据被认定为脏数据,dirty返回true。执行会话close方法时,若检测到dirtytrue,执行器会触发回滚操作,防止脏数据写入数据库,保证数据的一致性和完整性。

值得注意的是,若数据库的事务隔离级别设置为read uncommitted(读未提交),在数据插入操作后、关闭会话之前,数据库中能查询到新插入的记录。但执行sqlSession.close()时,MyBatis会根据autocommitdirty等变量状态判断,满足回滚条件时自动执行rollback()操作,事务回滚后,之前查询到的记录会从数据库中消失,维持数据的最终一致性。

四、总结

MyBatis的JdbcTransaction和纯粹的JDBC事务差别不大,只是扩展支持了连接池的connection。在开发过程中要明确,对数据库进行updatedeleteinsert操作时,必然是在事务中进行,这是数据库的设计规范。同时,本文剖析了MyBatis事务管理中的常见误区,希望能帮助大家更好地理解和运用MyBatis的事务管理机制,在实际项目中合理控制事务,保障数据的准确性和一致性。