Mybatis - Spring整合后回滚失效并且自动保存了?

前言

我当时在整理Mybatis的一个二级缓存问题:Mybatis - 单机器下二级缓存脏读问题的解决(TransactionalCache的运用)

当时写的项目案例中,我发现事务回滚无法失效,虽然结果上并不影响二级缓存的一个结论。但是这个问题一直困扰了我好久。最后看了源码才发现问题出在哪里。

一. 案例回顾

看下我的程序:

@PostMapping("/hello")
public User hello(@RequestBody User user) {
    SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) myApplicationContext.applicationContext.getBean("sqlSessionFactory");
    SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
    UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);

    User tom = mapper1.getUserById("tom");

    mapper1.insertUser(user);
    List<User> users = mapper1.getUsers();
    sqlSession1.rollback();

    List<User> ss = mapper2.getUsers();

    System.out.println("SqlSession1:" + users.size());
    System.out.println("SqlSession2:" + ss.size());

    return tom;
}

我们先不看这段代码有什么逻辑和意义,我们只关注sqlSession1.rollback();这段代码。理论上来说,如果这个代码回滚了,那么我们就应该把上面的insert操作也给回滚。但是实际上却不是这样。

这是我的application.yml文件:
在这里插入图片描述
我数据库中的数据:
在这里插入图片描述
此时我调用一下接口:
在这里插入图片描述
程序在跑到插入操作的时候,我们打个断点:
在这里插入图片描述
此时再看看数据库:
在这里插入图片描述
发现数据竟然直接插入了?这是什么鬼?

二. 案例分析

2.1 从rollback函数开始找问题

我这里是从rollback开始分析然后找到原因的:

sqlSession1.rollback();
↓↓↓↓↓
public class DefaultSqlSession implements SqlSession {
  @Override
  public void rollback(boolean force) {
    try {
      executor.rollback(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error rolling back transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
}
↓↓↓↓↓
public class CachingExecutor implements Executor {
  @Override
  public void rollback(boolean required) throws SQLException {
    try {
      delegate.rollback(required);
    } finally {
      if (required) {
        tcm.rollback();
      }
    }
  }
}
↓↓↓↓↓
public abstract class BaseExecutor implements Executor {
  @Override
  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }
}
↓↓↓↓↓
public class SpringManagedTransaction implements Transaction {
  @Override
  public void rollback() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      LOGGER.debug(() -> "Rolling back JDBC Connection [" + this.connection + "]");
      this.connection.rollback();
    }
  }
}

结果发现,程序在进行if判断的时候,根本走不到this.connection.rollback();。结果我调试一下,一看发现:
在这里插入图片描述
这个this.autoCommittrue。可见,他并不是我们代码中对于SqlSessionautoCommit属性,两个是不一样的东西。

看一下它的引用:

private void openConnection() throws SQLException {
  this.connection = DataSourceUtils.getConnection(this.dataSource);
  this.autoCommit = this.connection.getAutoCommit();
  this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

  LOGGER.debug(() -> "JDBC Connection [" + this.connection + "] will"
      + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
}

如图:发现这个connection的类型HikariProxyConnectionSpringBoot默认带的一个数据源。
在这里插入图片描述
而我们并没有在程序里面对HikariProxyConnection这个数据源做自动提交功能的设置。

同时我们可以看到程序的调用链里面存在着底层Connection的获取操作:
在这里插入图片描述
也就是说:

  1. Mybatis在整合Spring之后,其事务类型的类是SpringManagedTransaction
  2. 通过SpringManagedTransaction去拿到Connection链接的时候,SpringBoot默认的类型是HikariProxyConnection
  3. 因为HikariProxyConnection默认情况下autoCommit属性是true
  4. 因此案例中的代码,事务是自动提交的。

说白了,就是Mybatis整合Spring之后,自动提交属性是根据SpringManagedTransactionautoCommit属性。而不是sqlSessionFactory.openSession(false);

为了进一步证实这样的说法,我们再从commit去看这个问题

2.2 从 commit 事务提交去证实

首先我们看下sqlSessionFactory.openSession(false)这个函数有什么用:

public class DefaultSqlSessionFactory implements SqlSessionFactory {
  @Override
  public SqlSession openSession(boolean autoCommit) {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建一个DefaultSqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
}

public class DefaultSqlSession implements SqlSession {
  private final boolean autoCommit;
  private boolean dirty;
  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;
  }
}

可见最后就是创建了一个DefaultSqlSession对象实例,然后里面的dirty默认是true。并且autoCommit赋值为false。但是我们案例中有着insert操作,根据调用链,最后会执行到DefaultSqlSessionupdate函数,就是说明数据发生了更改,此时dirty赋值为true,我个人理解为就是有脏数据可能的意思。
在这里插入图片描述

那么我们再看显式地SqlSession.commit()操作:

public class DefaultSqlSession implements SqlSession {
  @Override
  public void commit() {
    commit(false);
  }
  ↓↓↓↓↓
  @Override
  public void commit(boolean force) {
    try {
      // force是false
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
}

可见isCommitOrRollbackRequired这个函数,对于当前SqlSessioncommit操作至关重要。他决定着当前SqlSession是否提交。也就是说我们的insert操作是否会成功。 再来看下它的源码:

// autoCommit我们在sqlSessionFactory.openSession(false)中设置为false
// dirty由于发生了数据的更改(增删改),改为true。
// force是传进来的默认值false
private boolean isCommitOrRollbackRequired(boolean force) {
  return (!autoCommit && dirty) || force;
}

那么在执行完insert操作后,Mybatis就会去判断当前SqlSession是否需要进行commit提交。而此时整个表达式的值是true,因此最后的执行结果会提交上去,最后同步到数据库。因此回滚功能就失效了。(因为自动提交了)。

最后从调用链来看,还需要注意一点的就是:

  • 先执行executor的一个事务提交,在执行SpringManagedTransaction的提交。
    在这里插入图片描述
    而我们的SQL执行是依赖于executor的一个提交操作。而SpringManagedTransaction的事务,我觉得更倾向于一种整体的业务逻辑。executor则面向的是单个的SQL执行操作。只不过executor的最终实现就是SpringManagedTransaction的commit操作。因此案例的插入操作是能够成功的。

因此,我们仅仅改变SqlSessionautoCommit属性是不够的,还需要改变数据源的autoCommit机制。 那么知道这个问题的本质之后,我们只需要做到一点:改变你项目中数据源的自动提交机制即可。

三. 问题解决

Spring配置文件中添加数据源自动提交属性的配置:
在这里插入图片描述
此时再进行测试:代码跑到插入操作之后。
在这里插入图片描述
此时数据库中的数据并没有生效:事务成功了。
在这里插入图片描述

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在使用mybatis-plus进行事务回滚时,需要注意以下几点: 1. 确保你的mybatis-plus版本是高于2021年下半年的版本,因为在高版本中,mybatis-plus已经实现了多数据源事务的统一提交和回滚管理。 2. 在使用mybatis-plus时,部分配置项的名称可能与mybatis有所不同,需要注意调整配置项的名称。 3. 当需要实现多数据源的事务支持时,mybatis-plus已经将相关功能集成到spring框架中,因此简化了多数据源切换和事务的回滚提交操作。 4. 如果你使用的是旧版本的mybatis-plus,那么你需要自己实现多数据源切换和事务的回滚提交等操作,相对来说比较麻烦。 综上所述,使用mybatis-plus进行事务回滚相对简单,特别是在高版本中已经实现了多数据源事务的统一管理。具体的实现细节可以参考相关文档或官方指南。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [解决seata和mybatis-plus事物不回滚问题](https://blog.csdn.net/dxcll/article/details/124132317)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [多数据源事务集成-mybatis-plus](https://blog.csdn.net/u013309797/article/details/121968596)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值