MyBatis事务管理详解:原理、场景与最佳实践
MyBatis作为一款广泛应用的持久层框架,其事务管理机制有着独特的设计与实现。今天,咱们就深入剖析MyBatis的事务管理,帮助大家全面掌握其中的核心逻辑。
一、事务基础概念
在深入探讨MyBatis事务管理之前,先简单回顾一下事务的基本概念。事务是数据库操作的一个逻辑单元,由一系列数据库操作组成。它必须满足数据库ACID特性中的一致性要求,即这些操作要么全部成功提交到数据库,要么在出现问题时全部回滚,绝不允许部分操作成功、部分操作失败的情况发生。
举个银行转账的例子,从一个账户扣款和向另一个账户存款这两个操作必须同时成功,才能保证资金的安全以及数据库中金额数据的一致性。如果其中一个操作成功,另一个失败,就会导致数据不一致,出现资金丢失或错误增加的情况。
二、MyBatis事务管理机制
MyBatis主要通过JdbcTransaction
和ManagedTransaction
这两种方式来实现事务控制。
(一)JDBC原生事务管理(JdbcTransaction)
JdbcTransaction
采用JDBC原生的事务管理方式,借助java.sql.Connection
对象来控制事务。在这种模式下,MyBatis从数据源获取Connection
对象,之后就需要开发者手动调用Connection
的commit()
方法提交事务,调用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()
等方法时,并不会自动控制事务,即不会出现commit
或rollback
操作。当单纯使用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
的概念,这就导致在程序中多次执行insert
、update
等操作时,会开启多个事务。例如:
// 执行了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的情况
以之前的代码为例,若将SqlSession
的autoCommit
属性设为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
方法通过判断autocommit
和dirty
两个关键变量来决定是否回滚。dirty
变量用于标识数据是否为脏数据,默认值为false
。执行数据更新、插入等操作后,dirty
的值会改变,若数据被认定为脏数据,dirty
返回true
。执行会话close
方法时,若检测到dirty
为true
,执行器会触发回滚操作,防止脏数据写入数据库,保证数据的一致性和完整性。
值得注意的是,若数据库的事务隔离级别设置为read uncommitted
(读未提交),在数据插入操作后、关闭会话之前,数据库中能查询到新插入的记录。但执行sqlSession.close()
时,MyBatis会根据autocommit
和dirty
等变量状态判断,满足回滚条件时自动执行rollback()
操作,事务回滚后,之前查询到的记录会从数据库中消失,维持数据的最终一致性。
四、总结
MyBatis的JdbcTransaction
和纯粹的JDBC事务差别不大,只是扩展支持了连接池的connection
。在开发过程中要明确,对数据库进行update
、delete
、insert
操作时,必然是在事务中进行,这是数据库的设计规范。同时,本文剖析了MyBatis事务管理中的常见误区,希望能帮助大家更好地理解和运用MyBatis的事务管理机制,在实际项目中合理控制事务,保障数据的准确性和一致性。