我当时在整理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文件;
我数据库中的数据;
此时我调用一下接口;
程序在跑到插入操作的时候;我们打个断点;
此时再看看数据库;
发现数据竟然直接插入了?这是什么鬼?
我这里是从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的autoCommit属性。而不是sqlSessionFactory.openSession(false);
为了进一步证实这样的说法;我们再从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操作;根据调用链;最后会执行到DefaultSqlSession的update函数;就是说明数据发生了更改;此时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这个函数;对于当前SqlSession的commit操作至关重要。他决定着当前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的提交。因此;我们仅仅改变SqlSession的autoCommit属性是不够的;还需要改变数据源的autoCommit机制。 那么知道这个问题的本质之后;我们只需要做到一点;改变你项目中数据源的自动提交机制即可。
在Spring配置文件中添加数据源自动提交属性的配置;
此时再进行测试;代码跑到插入操作之后。
此时数据库中的数据并没有生效;事务成功了。
Vue3---Pinia-状态管理(环境搭建安装及各属性使用教程)详细使用教程
女神联盟怎么召唤3个宠物同时出战?-女神联盟召唤3个宠物同时出战教程攻略