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.autoCommit 是true 。可见,他并不是我们代码中对于SqlSession 的autoCommit 属性,两个是不一样的东西。
看一下它的引用:
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 的类型HikariProxyConnection 是SpringBoot 默认带的一个数据源。  而我们并没有在程序里面对HikariProxyConnection 这个数据源做自动提交功能的设置。
同时我们可以看到程序的调用链里面存在着底层Connection 的获取操作:  也就是说:
Mybatis 在整合Spring 之后,其事务类型的类是SpringManagedTransaction 。- 通过
SpringManagedTransaction 去拿到Connection 链接的时候,SpringBoot 默认的类型是HikariProxyConnection 。 - 因为
HikariProxyConnection 默认情况下autoCommit 属性是true 。 - 因此案例中的代码,事务是自动提交的。
说白了,就是Mybatis 整合Spring 之后,自动提交属性是根据SpringManagedTransaction 的autoCommit 属性。而不是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);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx);
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 操作,根据调用链,最后会执行到DefaultSqlSession 的update 函数,就是说明数据发生了更改,此时dirty 赋值为true ,我个人理解为就是有脏数据可能的意思。 
那么我们再看显式地SqlSession.commit() 操作:
public class DefaultSqlSession implements SqlSession {
@Override
public void commit() {
commit(false);
}
↓↓↓↓↓
@Override
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
}
可见isCommitOrRollbackRequired 这个函数,对于当前SqlSession 的commit 操作至关重要。他决定着当前SqlSession 是否提交。也就是说我们的insert 操作是否会成功。 再来看下它的源码:
private boolean isCommitOrRollbackRequired(boolean force) {
return (!autoCommit && dirty) || force;
}
那么在执行完insert 操作后,Mybatis 就会去判断当前SqlSession 是否需要进行commit 提交。而此时整个表达式的值是true ,因此最后的执行结果会提交上去,最后同步到数据库。因此回滚功能就失效了。(因为自动提交了)。
最后从调用链来看,还需要注意一点的就是:
- 先执行
executor 的一个事务提交,在执行SpringManagedTransaction 的提交。  而我们的SQL 执行是依赖于executor 的一个提交操作。而SpringManagedTransaction 的事务,我觉得更倾向于一种整体的业务逻辑。executor 则面向的是单个的SQL 执行操作。只不过executor 的最终实现就是SpringManagedTransaction 的commit操作。因此案例的插入操作是能够成功的。
因此,我们仅仅改变SqlSession 的autoCommit 属性是不够的,还需要改变数据源的autoCommit 机制。 那么知道这个问题的本质之后,我们只需要做到一点:改变你项目中数据源的自动提交机制即可。
三. 问题解决
在Spring 配置文件中添加数据源自动提交属性的配置:  此时再进行测试:代码跑到插入操作之后。  此时数据库中的数据并没有生效:事务成功了。 
|