章
目
录
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的事务管理机制,在实际项目中合理控制事务,保障数据的准确性和一致性。