MyBatis 事务管理:一文掌握Mybatis事务管理核心逻辑

MyBatis中对事务处理主要有两种方式,一种是使用JDBC原生事务管理,另一种则是委托Spring框架来完成对事务的控制。虽然其事务管理实现方式简单,但其中所蕴藏的知识却不简单。

前言

我们平时所谈及的事务其实是可以看做是数据库操作的一个逻辑单元。它由一系列的数据库操作组成,为了确保满足数据库ACID中的一致性这些操作要么全部成功提交,要么全部回滚,不能存在部分成功部分失败的状态。

举个简单的例子,在一个银行转账业务操作中,涉及到从一个账户扣款和向另一个账户存款两个操作,这两个操作必须同时成功。只有这样才能有效保证资金的安全和数据库的一致性。

Mybatis的事务管机制

Mybatis作为主流的持久层框架,其主要通过JdbcTransactionManagedTransaction两种方式来实现对事务 的控制

  • 使用 JDBC 原生事务管理JdbcTransaction其实是使用 JDBC 原生的事务管理方式,通过java.sql.Connection对象来控制事务。在这种方式下,MyBatis 会从数据源获取一个Connection对象,然后由开发者手动调用Connectioncommit()方法来提交事务,调用rollback()方法来回滚事务。具体源码逻辑如下:
java 复制代码
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();
  }
}

// ... 省略其他方法
}
  • 使用 Spring 框架的事务管理ManagedTransaction 含义为托管事务,即其内部不会对事物进行管理,而是将事务控制托管给其它框架。例如,在Mybatis整合Spring框架的项目中,通常会借助 Spring 的事务管理机制来管理事务。
java 复制代码
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();
      }
     }

    // ... 省略其他方法
}

可以看到,在ManagedTransaction中它既不会提交也不会回滚一个连接,而是将事务的整个生命周期管理工作交由容器处理,像SpringJEE 应用服务器的上下文环境就可以充当这样的容器。

而当使用 springBoot 项目中当引入mybatis依赖后其实我们无需手动配置ManagedTransaction。这是因为在springboot-start中会根据项目中配置的数据源自动创建合适的事务管理器并注册到 Spring 容器中,进而直接使用 @Transactional 注解来实现事务控制。

事实上,无论是 SqlSession,还是 Executor,它们的事务方法,最终都指向了 Transaction 的事务方法,即都是由 Transaction 来完成事务提交、回滚的。

Mybatis中有关事务的几种特殊场景

理解 Mybatis中的手动事务控制

我们前面讲了Mybatis中的事务管理器,但我们还是要明确一点。那便是在 MyBatis 里,虽然所有的sql执行都委托于Executor但这不意味着Executor中的执行insert ()、update () 等方法时,其会显示控制事务 。换言之,在Executor中执行数据库操纵方法时,其并不会出现所谓的commit,rollback等操作。

而当我们单纯使用Mybatis框架时,我们的手动控制事务的方式大致如下:

java 复制代码
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 实例,false 表示关闭自动提交模式。之后在执行 SQL 操作后,若操作成功则调用 sqlSession.commit() 提交事务;若发生异常,则调用 sqlSession.rollback() 回滚事务。

再次强调,上述的事务控制其实是我们人为添加的,并不是框架内部处理的。也就是说,在后续我们分析Executor中有关数据操纵的insert ()、update () 等方法内部时,需要忘记事务的存在,更不要试图在执行器Executorinsert () 等方法内部寻找有关事务的任何方法。

sqlSession生命周期内可以多个事务

事实上,JDBC中并存在所谓的Session相关信息。这其实就使得在程序中执行多次insert,update操作的话,其实就会开启多个事务。

java 复制代码
            // 执行了connection.setAutoCommit(false),并返回
            SqlSession sqlSession = MybatisSqlSessionFactory.openSession();
		try {
			StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
			
			Student student = new Student();
			student.setName("yy");
			student.setEmail("[email protected]");
			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); 时,由于 setAutoCommit(false),此次插入操作被纳入当前事务中。
    • 随后调用 sqlSession.commit();,这使得第一次插入操作所在的事务被提交,第一次事务结束。
  • 第二次事务

    • 接着再次执行 studentMapper.insertStudent(student);,又开启了一个新的事务,该插入操作被包含在这个新事务里。
    • 之后调用 sqlSession.commit();,新事务被提交,第二次事务结束。

那如果在执行sql操作时出现异常,此时我们rollbak具体执行逻辑如下:

  1. 在第一次 insert 之后且第一次 commit 之前发生异常
java 复制代码
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();
}

当在第一次 insert 之后且第一次 commit 之前发生异常时,rollback 会回滚第一次 insert 操作。因为在第一次 commit 之前,第一次 insert 操作处于一个未提交的事务中,调用 rollback 会撤销该事务中的所有操作,也就是撤销第一次 insert 操作。

  1. 第二次 insert 之后、第二次 commit 之前发生异常
java 复制代码
try {

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

当、第二次 commit 之前发生异常时,rollback 会回滚第二次 insert 操作。这是因为第一次 insert 操作所在的事务已经通过 commit 提交,而第二次 insert 操作处于一个新的未提交事务中,调用 rollback 会撤销这个未提交事务中的操作,即撤销第二次 insert 操作,而此时并不会撤销第一次提交内容。

  1. 在第二次 commit 之后发生异常
java 复制代码
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();
}

当在第二次 commit 之后发生异常时,rollback 不会回滚任何操作。因为两次 insert 操作所在的事务都已经通过 commit 提交,此时不存在未提交的事务,调用 rollback 不会有任何效果。

总之,当autoCommit=false 时是自动开启事务的,执行 commit () 后,该事务结束 。以上代码正常情况下,开启了2 个事务,向数据库插入了 2 条数据。

由于JDBC 中不存在 Mybatis 中的 session 的概念,所以并不是说一个SqlSession整个生命周期只有一个事务,而是执行几次sql操作就有几个事务

具体到上述例子,执行几次insert语句,数据库就会有几条记录。切勿将SqlSessionConnection混淆,一个SqlSession生命周期内其实可以由多个事务,而而 rollback (),只能回滚当前未提交的事务。而不能回滚之前已提交事务。

关闭自动提交,但未执行Commit会发生什么

还以上述代码为例,只不过此时我们将SqlSession中的autoCommit 属性设定为false,即关闭SqlSession的自动提交。

java 复制代码
try {
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    studentMapper.insertStudent(student);
} finally {
    sqlSession.close();
}

此时与之前不同的是,我们在代码中并未手动 commit而仅执行close关闭当前会话,那如果不执行提交操作,仅调用会话的关闭方法,事务内部究竟会发生什么?

MyBatis 在架构设计时已充分考虑到此类情况。当执行close方法时,MyBatis 会进行一系列逻辑判断,并根据判断结果决定是否执行rollback操作。

SqlSession # close

java 复制代码
 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

java 复制代码
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。 另外,需要明确,无论你是否手动处理了事务,只要是对数据库进行任何update、delete、insert,都一定是在事务中进行的,这是数据库的设计规范之一。

同时,我们本文还就Mybatis中有关事务的两大常见误区,希望本文对你理解Mybatis事务管理有所帮助。

相关推荐
Nelson_hehe1 小时前
Java基础第四章、面向对象
java·语法基础·面向对象程序设计
Thomas_YXQ1 小时前
Unity3D Lua集成技术指南
java·开发语言·驱动开发·junit·全文检索·lua·unity3d
ShiinaMashirol2 小时前
代码随想录打卡|Day27(合并区间、单调递增的数字、监控二叉树)
java·算法
东阳马生架构3 小时前
Nacos简介—3.Nacos的配置简介
java
北极的企鹅883 小时前
XML内容解析成实体类
xml·java·开发语言
oioihoii3 小时前
C++23 中 static_assert 和 if constexpr 的窄化布尔转换
java·jvm·c++23
聂 可 以4 小时前
调整IntelliJ IDEA当前文件所在目录(包路径)的显示位置
java·ide·intellij-idea
东阳马生架构4 小时前
Sentinel源码—7.参数限流和注解的实现一
java·sentinel
李白的粉4 小时前
基于springboot的在线教育系统
java·spring boot·毕业设计·课程设计·在线教育系统·源代码
码农10087号4 小时前
Hot100方法及易错点总结2
java