多数据源情况下事务失效分析

1.失效基本原因

1.1没有配置事务管理器

在多数据源的情况下,需要为每个数据源,显式配置事务管理器,如下:

@Bean
public PlatformTransactionManager aDataSourceTransactionManager(@Qualifier("aDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}
@Bean
public PlatformTransactionManager bDataSourceTransactionManager(@Qualifier("bDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

1.2事务管理器名称相同

配置了事务管理器,但是 bean 名称相同,导致只有一个数据源的事务生效。这是因为 spring bean 名称相同时,只能保留一个bean,如下:

// bean 名称都是 dataSourceTransactionManager,spring 只会保留一个
@Bean
public PlatformTransactionManager dataSourceTransactionManager(@Qualifier("aDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}
@Bean
public PlatformTransactionManager dataSourceTransactionManager(@Qualifier("bDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

1.3Transactional 注解未指定事务管理器

Transactional 注解没有指明哪一个事务管理器,或没有配置默认的事务管理器。这种情况下,使用注解会报错,spring 不知道要用哪个事务管理器开启事务,如下:

@Transactional(rollbackFor = {Exception.class}) // 报错,未指明事务管理器
public void add(Object request) {
    // do something...
}

解决方法:

  1. 设置事务管理器:
@Transactional(value="aDataSourceTransactionManager", rollbackFor = {Exception.class}) 
// 使用 aDataSourceTransactionManager 事务管理器,对 aDataSource 的资源开启事务
public void add(Object request) {
    // do something...
}
  1. 配置默认事务管理器,使用时则可以不需要指定具体的事务管理器

配置默认事务管理器后,bDataSource 的事务还是需要显式指定,否则使用 aDataSource 的事务,对 b 来说依然是事务失效的

@Bean
@Primary // 默认使用 该事务处理器
public PlatformTransactionManager aDataSourceTransactionManager(@Qualifier("aDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}
@Bean
public PlatformTransactionManager bDataSourceTransactionManager(@Qualifier("bDataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

1.4同一个方法内使用两个数据源的操作

方法如下:

@Transactional(value="aDataSourceTransactionManager", rollbackFor = {Exception.class}) 
public void operateA(){
    // aDataSource operate...
    // bDataSource operate...
}

此种情况,只有 aDataSource 的操作是加了事务的,如果需要 bDataSource 也在事务中,则需要根据事务传播,开启一个新的事务

@Transactional(value="aDataSourceTransactionManager", rollbackFor = {Exception.class}) 
public void operateA(){
    // aDataSource operate...
    operateA();
}

// 通过事务传播,开启一个新的事务
// 注意,要创建新的类去操作,否则事务依然是失效的(同类当中的方法调用不走 aop 代理)
@Transactional(value="bDataSourceTransactionManager", rollbackFor = {Exception.class}, propagation= Propagation.REQUIRES_NEW) 
public void operateA(){
    // bDataSource operate...
}

2.基于 Mybatis-plus 多数据源方案

基础配置参考官网:https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611
其他与单数据源 myabtis 配置相同

3.Mybatis-plus 多数据源方案遇到的实际问题

3.1项目背景

在使用 mybatis-plus 的项目 A 中,由于各种原因,引用项目 B 和 C 的子项目(包含 mapper 和 基础service),B和C 的子项目中,包含了他们各自项目的数据源信息,因此,A 项目中包含多个数据源。结构如下
在这里插入图片描述

项目 B和C 的配置大致如下:

B 项目Mybatis plus 配置

@Slf4j
@Configuration("bMybatisPlusConfig")
@MapperScan(
        basePackages = "com.test.b.data.dao",
        sqlSessionTemplateRef = "bSqlSessionTemplate")
public class MybatisPlusConfig {

    @Bean("bDataSource")
    public DataSource dataSource() throws Exception {
        // 忽略创建配置
        return createDataSource();
    }
    
    @Bean("bSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("bDataSource") DataSource dataSource) throws Exception {
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setMapUnderscoreToCamelCase(false); // 数据库与bean字段名一致
        configuration.setLocalCacheScope(LocalCacheScope.STATEMENT); // 关闭一级缓存
        configuration.setCacheEnabled(false); // 关闭二级缓存
        configuration.setLogImpl(StdOutImpl.class);


        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setConfiguration(configuration);
        factoryBean.setDataSource(dataSource);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factoryBean.setMapperLocations(resolver.getResources("classpath*:b-mapper/**Mapper.xml"));
        factoryBean.setTypeAliasesPackage("com.test.b.data.dao.model");

        //mybatis 分页插件
        Interceptor[] plugins = {mybatisPlusInterceptor()};
        factoryBean.setPlugins(plugins);
        return factoryBean.getObject();
    }

    @Bean("bSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("bSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public DataSourceTransactionManager bDataSourceTransactionManager(@Qualifier("bDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

C 项目Mybatis plus 配置

与 B 项目相同,仅名称配置变化

@Slf4j
@Configuration("cMybatisPlusConfig")
@MapperScan(
        basePackages = "com.test.c.data.dao",
        sqlSessionTemplateRef = "cSqlSessionTemplate")
public class MybatisPlusConfig {

    @Bean("cDataSource")
    public DataSource dataSource() throws Exception {
        // 忽略创建配置
        return createDataSource();
    }
    
    @Bean("cSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("cDataSource") DataSource dataSource) throws Exception {
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setMapUnderscoreToCamelCase(false); // 数据库与bean字段名一致
        configuration.setLocalCacheScope(LocalCacheScope.STATEMENT); // 关闭一级缓存
        configuration.setCacheEnabled(false); // 关闭二级缓存
        configuration.setLogImpl(StdOutImpl.class);


        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setConfiguration(configuration);
        factoryBean.setDataSource(dataSource);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factoryBean.setMapperLocations(resolver.getResources("classpath*:c-mapper/**Mapper.xml"));
        factoryBean.setTypeAliasesPackage("com.test.c.data.dao.model");

        //mybatis 分页插件
        Interceptor[] plugins = {mybatisPlusInterceptor()};
        factoryBean.setPlugins(plugins);
        return factoryBean.getObject();
    }

    @Bean("cSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("cSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public DataSourceTransactionManager cDataSourceTransactionManager(@Qualifier("cDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

3.2多数据源及Mybatis-plus 引发的问题

如果是正常在项目中使用事务 @Transactional注解,配置对应的事务管理器,则事务可以正常执行。
但是当我们使用了 Mybatis-plus 包中的 ServiceImpl 时,则可能会发生一些问题
mybatis-plus SeviceImpl 部分方法

public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean saveBatch(Collection<T> entityList, int batchSize) {
        String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
        return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
    }
    
    .....
}

可以看到,SeviceImpl 的部分方法是加了 @Transactional注解的,但是他并没有指定事务管理器。
在多数据源,配置多个事务管理器的的情况下,不指定具体的事务管理器,根据我们第一节的描述,则会发生报错。由于我们正常情况下无法直接修改mybatis-plus 的源码,因此在此种情况下,我们可以通过如下方法解决:

  1. 配置默认事务管理器
    1. 优点:可以不指定事务管理器,也能加入到事务中
    2. 缺点:只有默认数据源的事务才生效,非默认数据源的依然是失效状态
  2. 重写 saveBatch 方法,直接指定事务管理器
    1. 优点:简单直接
    2. 缺点:工作量大,每个添加了@Transactional注解的地方都需要重写,后续添加新的ServiceImpl子类也需要重写
  3. 不调用添加@Transactional注解的方法
    1. 缺点:增加很多轮子代码,后续使用不太方便
  4. 使用 mybatis-plus 官方提供的动态数据源方案
    1. 优点:只需要添加一个注解,即可完成数据源事务的切换
    2. 缺点:每个用到事务操作或数据库操作的类上,都需要增加该注解(主要是 mapper 和 ServiceImpl 子类)

3.3动态数据源配置

因为项目依赖问题,不方便在 B和C 项目中做多数据源管理,也不方便做配置变更。
因此,让项目B和C 数据源模块 依赖 dynamic-datasource-spring-boot-starter 包,然后在对应 mapper 和 service 上加 @DS多数据源注解,不影响当前项目使用,但可以帮助 项目A 做多数据源处理。

A项目 Mybatis plus配置

@Configuration
@Import(DynamicDataSourceAutoConfiguration.class)
public class MybatisDataSourceConfig implements InitializingBean {

    @Autowired
    private ApplicationContext context;

    @Bean
    public DataSource dataSource(ApplicationContext context) {
        // 由于项目依赖问题,无法通过原生方式配置动态数据源,这里根据已配置好的数据源,自己生成一个实例
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        // 配置默认数据源,可以不指定(在没有加 DS 注解的地方会报错)
//        dataSource.setPrimary("xxxDataSource");
        Map<String, DataSource> beans = context.getBeansOfType(DataSource.class);
        for (Map.Entry<String, DataSource> entry : beans.entrySet()) {
            dataSource.addDataSource(entry.getKey(), entry.getValue());
        }
        return dataSource;
    }

    @Bean
    @Primary
    public PlatformTransactionManager primaryTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        // 配置默认事务管理器,数据源对象为当前动态数据源
        return new DataSourceTransactionManager(dataSource);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 替换依赖的 项目B和C 中 mybatis的 SqlSessionFactory 的数据源
        // 如果不替换为当前配置的动态数据源,则事务会失效
        Map<String, SqlSessionFactory> sessionFactoryMap = context.getBeansOfType(SqlSessionFactory.class);
        DataSource dataSource = context.getBean("dataSource", DataSource.class);
        for (Map.Entry<String, SqlSessionFactory> entry : sessionFactoryMap.entrySet()) {
            SqlSessionFactory sessionFactory = entry.getValue();
            org.apache.ibatis.session.Configuration configuration = sessionFactory.getConfiguration();
            Environment environment = configuration.getEnvironment();
            configuration.setEnvironment(new Environment(environment.getId(), environment.getTransactionFactory(), dataSource));
        }
    }
}

3.4使用方法

配置完成后,我们就可以添加 DS 注解到对应 service 上,正常使用数据源以及事务了

@Service
@Slf4j
// 当前类使用 xxDataSource 数据源
@DS("xxDataSource")
public class xxServiceImpl implements xxService {

    // 使用默认事务管理器即可
    // 根据 DS 注解自动选择对应数据源的事务
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public void add(){
        // operate ....
    }
}

4Mybatis-plus 动态数据源原理分析

4.1数据源

mybatis-plus 动态数据源的实现思路是:在实际使用时,使用动态数据源,而动态数据源则代理路由多个真实数据源。这样在获取连接时,使用方不需要做其他判断,交由动态数据源去判断使用哪个真实数据源。

4.2判断路由

动态数据源如何路由到真实数据源呢?
实际上是通过 DS 注解写的数据源名称判断的。加了 DS 注解的类会被代理,然后 Mybatis-plus 会在 DynamicDataSourceAnnotationInterceptor 类中,将 dsKey 做压栈处理,到后续需要从动态数据源获取连接时,则从栈中取出dsKey,根据 dsKey 路由到真实数据源。

4.3连接上下文

在spring中,事务是如何生效的呢?
原因在于 spring 事务会将连接资源放入TransactionSynchronizationManager.resources 线程上下文中,在开启事务或者数据库操作时,首先会将当前的数据源作为 key 在线程上下文中获取 ConnectionHolder,如果存在则使用 ConnectionHolder 中的连接,这样可以保证一个线程中,使用的是同一个连接,也就保证了事务是生效的。

在我碰到的项目实际问题当中,由于是依赖的子项目,子项目中的 mybatis 的 SqlSessionFactory 使用的数据源是真实数据源,如果在A项目中不修改 SqlSessionFactory 的数据源的话,会导致将真实数据源作为key,获取不到前面事务开启的连接,也就会导致事务失效。

具体源码位置:
Transactional 注解:org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
开启事务时会先获取连接
Mybatis:org.mybatis.spring.transaction.SpringManagedTransaction#openConnection
使用 spring的事务管理器时,会调用 DataSourceUtils.getConnection(this.dataSource) 方法,获取连接的同时将连接设置到线程上下文。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值