mybatisplus多数据源事务问题

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 方法执行的几个步骤
  1. 在加了@Transactional的事务注解下,spring会先扫描事务注解,使用动态代理开启事务管理。 org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
  2. 开启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.解决思路

这里的解决方案有三种:

  1. 通过实体类上面的@TableName(value = "xxx",schema = "schema ") “schema” + “table” 的方式可以保证数据不会走错,但是前提就是两个数据是要在同一个库里面,配置的时候也可以只配置一个数据源,通过前缀和表名区分,但是这样的话,两个数据源就绑定在一起了,如果想要拆开的话就比较麻烦,不符合松耦合的思想,所以此方法并不建议。
  2. 可以利用事务的传播方式:@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 
  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
Mybatis-Plus 多数据源事务配置可以通过 Spring Boot 中的 TransactionAutoConfiguration 自动配置类来实现。 1. 首先,在 application.properties 或 application.yml 中配置多个数据源的相关信息。 ``` # 数据源 1 spring.datasource.datasource1.url=jdbc:mysql://localhost:3306/db1 spring.datasource.datasource1.username=root spring.datasource.datasource1.password=123456 spring.datasource.datasource1.driver-class-name=com.mysql.jdbc.Driver # 数据源 2 spring.datasource.datasource2.url=jdbc:mysql://localhost:3306/db2 spring.datasource.datasource2.username=root spring.datasource.datasource2.password=123456 spring.datasource.datasource2.driver-class-name=com.mysql.jdbc.Driver ``` 2. 创建多个数据源的 DataSource 对象,并注册到 Spring 的 Bean 容器中。 ``` @Configuration public class DataSourceConfig { @Bean(name = "dataSource1") @ConfigurationProperties(prefix = "spring.datasource.datasource1") public DataSource dataSource1() { return DataSourceBuilder.create().build(); } @Bean(name = "dataSource2") @ConfigurationProperties(prefix = "spring.datasource.datasource2") public DataSource dataSource2() { return DataSourceBuilder.create().build(); } } ``` 3. 配置 Mybatis-Plus 的 SqlSessionFactory 和 TransactionManager,指定对应的数据源。 ``` @Configuration @MapperScan(basePackages = {"com.example.mapper"}) public class MybatisPlusConfig { @Bean(name = "sqlSessionFactory1") public SqlSessionFactory sqlSessionFactory1(@Qualifier("dataSource1") DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); return sqlSessionFactory.getObject(); } @Bean(name = "sqlSessionFactory2") public SqlSessionFactory sqlSessionFactory2(@Qualifier("dataSource2") DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource); return sqlSessionFactory.getObject(); } @Bean(name = "transactionManager1") public DataSourceTransactionManager transactionManager1(@Qualifier("dataSource1") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "transactionManager2") public DataSourceTransactionManager transactionManager2(@Qualifier("dataSource2") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } } ``` 4. 在需要进行事务管理的方法上使用 @Transactional 注解,并指定对应的 TransactionManager。 ``` @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper1; @Autowired private UserMapper userMapper2; @Transactional(transactionManager = "transactionManager1") public void addUser1(User user) { userMapper1.insert(user); } @Transactional(transactionManager = "transactionManager2") public void addUser2(User user) { userMapper2.insert(user); } } ``` 这样,就可以实现多数据源事务管理了。注意,如果需要在同一个方法中操作多个数据源,需要手动处理事务
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值