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 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 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 dataSourceMap;
                  public static final ThreadLocal DATA_SOURCE = new ThreadLocal<>();
                  public SystemDynamicDatasource(Map 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);
                  }
                  

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


                  搞定收工!