mybtaisplus多数据源事务问题分析
文章目录
一.背景
1.背景问题
在项目中,通过使用mybatis-plus操作多数据源的时候,一旦加上事务,会报 Table 'xxx' doesn't exist
异常,原因是在切换数据源的时候,没有切换成功,下面会有所解释。以下是spring版本和mybatis-plus的版本
- mybatis-plus 3.3.1
- springboot 2.2.6
- spring-framework 5.2.5
2.问题所在
2.1 首先关注一下mybatis-plus数据源切换的几个类
//使用threadlocal维护一个当前数据源栈的,实现方法嵌套方法的不同数据源存储
com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder
com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder#peek
com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder#push
//Mybatis-plus内部通过维护数据源列表,和实现选择数据源方法来切换数据源
com.baomidou.dynamic.datasource.DynamicRoutingDataSource
com.baomidou.dynamic.datasource.DynamicRoutingDataSource#determineDataSource
com.baomidou.dynamic.datasource.DynamicRoutingDataSource#getDataSource
2.2 方法执行的几个步骤
- 在加了@Transactional的事务注解下,spring会先扫描事务注解,使用动态代理开启事务管理。
org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
- 开启sqlsession,
org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke
/**
* Proxy needed to route MyBatis method calls to the proper SqlSession got from Spring's Transaction Manager It also
* unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to pass a {@code PersistenceException} to the
* {@code PersistenceExceptionTranslator}.
*/
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//通过mybatis的SqlSessionTemplate配置的默认sqlSessionFactory来生成sqlSession
//如果是mybatisplus,sqlSessionFactory则是在MybatisPlusAutoConfiguration里配置
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
3.获取连接,org.apache.ibatis.executor.BaseExecutor#getConnection
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
//从transaction.getConnection()进入,可以知道,如果从事务对象里面获取到的连接为空的话,就会从mybatis-plus
//配置的数据源里面获取,也就是com.baomidou.dynamic.datasource.AbstractRoutingDataSource#getConnection()
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
从这一步可以看到,如果是未加事务的前提下是能够完成切换数据源的,但是一旦加了事务,在获取sqlsession的时候会从SqlSessionHolder里面获取,也就是说,获取到的sqlsession是上一个使用过的session,因为connection也是不为空的,因此不会去执行 openConnection()这个操作,所以切换数据源就失效了。
可以理解,事务是要在同一个sqlsession里面才能保证原子性。
另外通过查看mybatis-plus官方文档可以发现:
3.解决思路
这里的解决方案有三种:
- 通过实体类上面的
@TableName(value = "xxx",schema = "schema ")
“schema” + “table” 的方式可以保证数据不会走错,但是前提就是两个数据是要在同一个库里面,配置的时候也可以只配置一个数据源,通过前缀和表名区分,但是这样的话,两个数据源就绑定在一起了,如果想要拆开的话就比较麻烦,不符合松耦合的思想,所以此方法并不建议。 - 可以利用事务的传播方式:
@Transactional(propagation = Propagation.REQUIRES_NEW)
由刚才debug可以发现,如果是在同一个事务里面,sqlsession是沿用同一个,导致connection不会切换,因此无法切换数据源,如果在内层使用事务已经将传播方式设置为@Transactional(propagation = Propagation.REQUIRES_NEW)
,那么就会当做另起一个新的事务,也就会重新获取连接,而且在内层方法里面如果抛出异常的话,也会导致外层事务回滚的。但是如果内层事务执行完成,外层再抛出异常,则内层事务无法回滚,因为内层事务已经提交了。因此,如果要使用这种方式,需要将内层事务逻辑放在最后。
@Transactional
@Override
public void test1() {
boolean b = saveOrUpdate(new TradeGoods().setTradeGoodsId(2L).setSalePrice(new BigDecimal("0.03")));
// boolean b1 = goodsService.saveOrUpdate(new Goods().setGoodsId(10010L).setRemark("123"));
// int i = 1/0;
goodsService.test2();
int i = 1/0;
}
// 下面代码是在 goodsService里面,这里是两个类的代码,为了方便就放在一起了。
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void