Spring Boot整合Mybatis配置多数据源

Spring Boot整合Mybatis配置多数据源

  • 前言
  • 一、固定数据源配置
  • 二、动态数据源
  • 搞定收工!

前言

在之前的事件管理系统博客中有提到动态的多数据源配置

工作中难免需要做几个工具方便自己偷懒,加上之前的挡板,数据源肯定没法单一配置,所以需要多数据源配置。这里介绍两种配置:动态数据源和固定数据源模式。这两种我在目前的工作的工具开发中都有用到。


一、固定数据源配置

Mybatis是提供这种固定的多数据源配置的,需要分别配置包扫描(一般是不同的数据源扫描不同的包),事务处理器等。

  • yml配置,主要是不要用Spring Boot自带的数据库配置,spring.datasource,或者其他数据源配置,改用自己的,这样其实Spring boot的数据库自动配置DataSourceAutoConfiguration其实是失效了的。
## 这个是第一个固定配置
spring:
  datasource:
    druid:
      db-type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.ibm.db2.jcc.DB2Driver
      url: jdbc:db2://*****
      username: ****
      password: ****
      initial-size: 1
      min-idle: 1
      max-active: 1
      max-wait: 5000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      max-evictable-idle-time-millis: 900000
      connection-error-retry-attempts: 1
      break-after-acquire-failure: true
## 这是第二个动态数据源配置
app:
  datasource:
    mapDatasource:
      TESTDATASOURCE1:
        db-type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
        url: jdbc:sqlserver://****
        username: ****
        password: ****
        initial-size: 5
        min-idle: 5
        max-active: 10
        max-wait: 5000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        max-evictable-idle-time-millis: 900000
        connection-error-retry-attempts: 1
        break-after-acquire-failure: true
      TESTDATASOURCE2:
        db-type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
        url: jdbc:sqlserver://****
        username: ****
        password: ****
        initial-size: 5
        min-idle: 5
        max-active: 10
        max-wait: 5000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        max-evictable-idle-time-millis: 900000
        connection-error-retry-attempts: 1
        break-after-acquire-failure: true
      TESTDATASOURCE3:
        db-type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.ibm.db2.jcc.DB2Driver
        url: jdbc:db2://****
        username: ****
        password: ****
        initial-size: 5
        min-idle: 5
        max-active: 10
        max-wait: 5000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        max-evictable-idle-time-millis: 900000
        connection-error-retry-attempts: 1
        break-after-acquire-failure: true
      TESTDATASOURCE4:
        db-type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.ibm.db2.jcc.DB2Driver
        url: jdbc:db2://****
        username: ****
        password: ****
        initial-size: 5
        min-idle: 5
        max-active: 10
        max-wait: 5000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000
        max-evictable-idle-time-millis: 900000
        connection-error-retry-attempts: 1
        break-after-acquire-failure: true
  • Spring Boot配置类
@Data
@ConfigurationProperties(prefix = "app.datasource")
public class SystemDynamicDatasourceProperties {
    private Map<String, DruidDataSource> mapDatasource;
}

  • mybatis固定数据源配置
@Configuration
public class DataSourceConfiguration {


    @Configuration
    @MapperScan(basePackages = "com.test.mapper.datasource1", sqlSessionTemplateRef  = "source1SqlSessionTemplate")
    public static class source1DatasourceConfiguration {
        @Bean(name = "source1DataSource")
        @ConfigurationProperties(prefix = "spring.datasource.druid")
        public DruidDataSource source1DataSource(){
            return new DruidDataSource();
        }

        @Bean(name = "source1TransactionManager")
        public DataSourceTransactionManager source1TransactionManager(@Qualifier("source1DataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }


        @Bean(name = "source1SqlSessionFactory")
        public SqlSessionFactory source1SqlSessionFactory(@Qualifier("source1DataSource") DataSource dataSource) throws Exception {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/source1/*.xml"));
            bean.setTypeAliasesPackage("com.source1.entity");
            return bean.getObject();
        }

        @Bean(name = "source1SqlSessionTemplate")
        public SqlSessionTemplate source1SqlSessionTemplate(@Qualifier("source1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }

    @Configuration
    @EnableConfigurationProperties(SystemDynamicDatasourceProperties.class)
    @MapperScan(basePackages = "com.source1.source1web.mapper.other", sqlSessionTemplateRef  = "otherSqlSessionTemplate")
    public static class DynamicDatasourceConfiguration {

        @Resource
        private SystemDynamicDatasourceProperties systemDynamicDatasourceProperties;
        @Bean(name = "otherDataSource")
        public SystemDynamicDatasource otherDataSource(){
            HashMap<Object, Object> map = new HashMap<>(systemDynamicDatasourceProperties.getMapDatasource());
            SystemDynamicDatasource systemDynamicDatasource = new SystemDynamicDatasource(map);
            return systemDynamicDatasource;
        }

        @Bean(name = "otherTransactionManager")
        public DataSourceTransactionManager otherTransactionManager(@Qualifier("otherDataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }


        @Bean(name = "otherSqlSessionFactory")
        public SqlSessionFactory otherSqlSessionFactory(@Qualifier("otherDataSource") DataSource dataSource) throws Exception {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/other/*.xml"));
            bean.setTypeAliasesPackage("com.source1.source1web.entity");
            return bean.getObject();
        }

        @Bean(name = "otherSqlSessionTemplate")
        public SqlSessionTemplate otherSqlSessionTemplate(@Qualifier("otherSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    }
}

  • 说明
    这两种其实就已经是两种数据源的配置了,当使用com.test.mapper.datasource1包下的Mapper的时候,使用的是就Datasource1的数据源,包括事务管理器,当使用com.source1.source1web.mapper.other包下的Mapper的时候,就是第二种数据源。

但是,这种只适合于单数据库操作的事务,多数据库的事务属于分布式事务,不适于此,当一个数据库事务提交成功之后,另一个事务失败的话,无法回滚第一个。因为此项目只适用于查询和单数据库的插入,失败不做回滚。

二、动态数据源

其实就是在上面的基础上,上面已经配置好了数据源,和动态的配置,但是漏掉了一些配置的细节,就是动态数据源,其实Mybatis提供了动态数据源的抽象类AbstractRoutingDataSource,我们只需要继承这个类并重写determineCurrentLookupKey方法,找到相关的数据源即可。在这个配置里无论添加多少数据源都可以,动态添加也是可以的。

  • 动态数据源配置
public class SystemDynamicDatasource extends AbstractRoutingDataSource {
    private Map<Object,Object> dataSourceMap;
    public static final ThreadLocal<String> DATA_SOURCE = new ThreadLocal<>();

    public SystemDynamicDatasource(Map<Object, Object> dataSourceMap){
        this.dataSourceMap = dataSourceMap;
        super.setTargetDataSources(dataSourceMap);
        super.afterPropertiesSet();
    }

    public void setDataSource(Integer key, DataSource dataSource){
        DruidDataSource oldDataSource = (DruidDataSource) dataSourceMap.put(key, dataSource);
        if (oldDataSource != null) {
            oldDataSource.close();
        }
        afterPropertiesSet();
    }
    public void removeDataSource(String key){
        DruidDataSource oldDataSource = (DruidDataSource) dataSourceMap.remove(key);
        if (oldDataSource != null) {
            oldDataSource.close();
        }
        afterPropertiesSet();
    }
    public boolean isExist(String key){
        return dataSourceMap.get(key) != null;
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DATA_SOURCE.get();
    }
    public void setDataSource(String dataSource){
        DATA_SOURCE.set(dataSource);
    }

    public static void removeDataSource(){
        DATA_SOURCE.remove();
    }
}

说明:

  • 线上使用进入多线程环境,其实主要区别就是需要确定当前线程使用的是哪个数据源。Map里面存储的就是多数据源,其中key是每个数据源的key,当某个线程需要确定使用哪个数据源的时候,就是靠这个key来进行区分的。
  • ThreadLocal就是确定某个线程使用的是哪个key,这样保证了线程安全,不会相互影响,只要使用的时候注意remove即可。
  • determineCurrentLookupKey调用来决定哪个数据源。
  • AOP配置

    这里最好使用AOP进行统一配置,不要在代码里写,在代码里写既添加了大量重复代码,而且与业务相关,代码可读性差,最好做成AOP统一配置。

    代码如下:

    • 注解
      @Target({ElementType.TYPE, ElementType.METHOD})
      public @interface OtherDatasource {
          String value() default "";
      }
      

      可以通过这个注解来充当切点,但是本次使用仅作为额外数据,获取指定的数据源使用。

    • 切面
    @Aspect
    @Component
    @Slf4j
    public class OtherDataSourceAspect {
    
        @Autowired
        private SystemDynamicDatasource systemDynamicDatasource;
    
    //     @Pointcut("@annotation(com.ibank.im.app.aop.cache.annotation.SystemCacheable)")
    
        @Pointcut("execution(public * com.test.mapper.other.*.*(..))")
        public void pointcut(){}
    
        @Around("pointcut()")
        public Object systemCacheableAround(ProceedingJoinPoint joinPoint) throws Throwable {
            Class<?> targetCls=joinPoint.getTarget().getClass();
    
            OtherDatasource annotation = targetCls.getAnnotation(OtherDatasource.class);
            String datasource = null;
            if (Objects.isNull(annotation) || !StringUtils.hasText(datasource = annotation.value())) {
                MethodSignature methodSignature=(MethodSignature)joinPoint.getSignature();
                Method targetMethod=
                        targetCls.getDeclaredMethod(
                                methodSignature.getName(),
                                methodSignature.getParameterTypes());
                OtherDatasource methodAnnotation = targetMethod.getAnnotation(OtherDatasource.class);
    
                if (Objects.isNull(methodAnnotation) || !StringUtils.hasText(datasource = methodAnnotation.value())) {
                    Object[] args = joinPoint.getArgs();
                    if (Arrays.isNullOrEmpty(args)) {
                        throw new IllegalArgumentException("must have 1 param");
                    }
                    if (!(args[0] instanceof String)) {
                        throw new IllegalArgumentException("the first param must be databaseEnv");
                    }
                    datasource = (String) args[0];
                }
            }
    
            if (!systemDynamicDatasource.isExist(datasource)) {
                throw new IllegalArgumentException("databaseEnv does not exist");
            }
            try{
                systemDynamicDatasource.setDataSource(datasource);
                return joinPoint.proceed();
            }finally {
                systemDynamicDatasource.removeDataSource(datasource);
            }
        }
    }
    

说明:

  • 本次数据源直接在Mapper层使用,不在Service层使用,因为一个Service可能要使用多个不同的数据源操作,比较麻烦,直接作用在Mapper层比较合适。
  • 逻辑上,先判断这个类上有没有注解,有的话使用这个注解,如果没有在使用方法上的注解,方法上如果没有注解,就是用第一个String参数,在没有就会报错,在判断是否存在这个数据源。不存在直接报错。
  • 使用的时候,一定要用try包裹,使用完成必须remove掉当前的值,无论是否发生异常。不移除的话容易发生内存溢出等问题。
  • 切面执行方法就是ProceedingJoinPoint类的proceed方法,但是实际上这个方法有两个重载的函数,一个带参数一个不带参数,这里简要介绍一下:
    • 不带参数的:表示调用时传递什么参数,就是什么参数,Advice不干预,原样传递。因为本次过程不修改什么参数。所以使用的是这个
    • 带参数的:自然就是相反的,将替换掉调用时传递的参数,这时候方法里调用的就是切面里的参数。

因为目前业务需求问题,都是使用的参数进行传递,所以只能定义在方法参数上。像这个样子:

@Mapper
public interface TestMapper {

    int insertTest(String env, Entity entity);

}

第一个参数就决定是哪个数据源,但是实际上业务并不采用。因为无法固定使用某个数据源的问题,只能以参数的方式传递。


搞定收工!