MyBatis事务管理详解:原理、场景与最佳实践

后端 潘老师 1小时前 3 ℃ (0) 扫码查看

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


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/18050.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】