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配置类

java 复制代码
@Data
@ConfigurationProperties(prefix = "app.datasource")
public class SystemDynamicDatasourceProperties {
    private Map<String, DruidDataSource> mapDatasource;
}
  • mybatis固定数据源配置
java 复制代码
@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方法,找到相关的数据源即可。在这个配置里无论添加多少数据源都可以,动态添加也是可以的。

  • 动态数据源配置
java 复制代码
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统一配置。

    代码如下:

    • 注解

      java 复制代码
      @Target({ElementType.TYPE, ElementType.METHOD})
      public @interface OtherDatasource {
          String value() default "";
      }

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

    • 切面

    java 复制代码
    @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不干预,原样传递。因为本次过程不修改什么参数。所以使用的是这个
    • 带参数的:自然就是相反的,将替换掉调用时传递的参数,这时候方法里调用的就是切面里的参数。

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

java 复制代码
@Mapper
public interface TestMapper {

    int insertTest(String env, Entity entity);

}

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


搞定收工!

相关推荐
用户908324602731 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840822 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解2 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解2 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记2 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者3 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840823 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解3 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者4 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺4 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端