springboot动态数据源的两种方式

236 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

日常工作中我们在一个springboot项目中,经常会有不同数据源的场景,那就需要配置动态数据源 dynamic-datasource,如果我们项目中用的是mybatis-plus,那就很方便去配置,如果用的是mybatis需要手动配置不同的数据源,指定不能的mapper扫描包,也可以自己实现一个动态切换数据源(spring已经给我们提供了一个接口)

一、springboot+mybatis

1、引入依赖

<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.0.0</version>
</dependency>
<!--druid连接池-->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.9</version>
</dependency>

2、配置yml文件

spring:
  datasource:
    druid:
      mysqlBeyond
        type: com.alibaba.druid.pool.xa.DruidXADataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql:${MYSQL_HOST}:${MYSQL_PORT}/${DATABASE_NAME}?useSSL=false&useUnicode=true&characterEncoding=UTF-8
        username: ${MYSQL_USER:root}
        password: ${MYSQL_PASS:root}
        initialSize: ${global.druid.initial-size}
        minIdle: ${global.druid.minIdle}
        maxActive: ${global.druid.maxActive}
        # 配置获取连接等待超时的时间
        maxWait: ${global.druid.maxWait}
     mysql:
        type: com.alibaba.druid.pool.xa.DruidXADataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql:${SLAVE1_MYSQL_HOST}:${SLAVE1_MYSQL_PORT}/${SLAVE1_DATABASE_NAME}?useSSL=false&useUnicode=true&characterEncoding=UTF-8
        username: ${SLAVE1_MYSQL_USER:root}
        password: ${SLAVE1_MYSQL_PASS:root}
        initialSize: ${global.druid.initial-size}
        minIdle: ${global.druid.minIdle}
        maxActive: ${global.druid.maxActive}
        maxWait: ${global.druid.maxWait}

3、分开配置数据源

mysqlBeyond配置类MySQLBeyondDataSourceConfig:

@Configuration
@MapperScan(basePackages = MySQLBeyondDataSourceConfig.BASEPACKAGES, sqlSessionFactoryRef = "mysqlBeyondSqlSessionFactory")
public class MySQLBeyondDataSourceConfig {
   
   public static final String BASEPACKAGES = "com.test.job.**.mysqlbeyond.mapper";
   private static final String MYSQLMAPPER = "classpath:mybatis/mapper/mysqlbeyond/*.xml";
   
   @Bean(name = "mysqlBeyond")
   @ConfigurationProperties(prefix = "spring.datasource.druid.mysqlbeyond")
   public DataSource dataSource() {
      return DruidDataSourceBuilder.create().build();
   }
   
    @Bean(name = "mysqlBeyondTransactionManager")
    public DataSourceTransactionManager masterTransactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
    
    @Bean(name = "mysqlBeyondSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("mysqlBeyond") DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MYSQLMAPPER));
        return sessionFactoryBean.getObject();
    }
   
}

mysql配置类MySQLDataSourceConfig:

@Configuration
@MapperScan(basePackages = MySQLDataSourceConfig.BASEPACKAGES, sqlSessionFactoryRef = "mysqlSqlSessionFactory")
public class MySQLDataSourceConfig {
   
   public static final String BASEPACKAGES = "com.onewo.job.executor.**.mysql.mapper";
   private static final String MYSQLMAPPER = "classpath:mybatis/mapper/mysql/*.xml";
   
   @Bean(name = "mysql")
   @ConfigurationProperties("spring.datasource.druid.mysql")
    @Primary
   public DataSource dataSource() {
      return DruidDataSourceBuilder.create().build();
   }
   
    @Bean(name = "mysqlTransactionManager")
    @Primary
    public DataSourceTransactionManager masterTransactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
    
    @Bean(name = "mysqlSqlSessionFactory")
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("mysql") DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MYSQLMAPPER));
        return sessionFactoryBean.getObject();
    }
   
}

二、springboot+mybati-plus

1、引入依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

2、一般使用情况下使用@DS("数据源名称"),我们进行切换数据源,只需要在配置文件中配置一组DataSource就可以了。

@DS("slave")
public List<User> list(String area) {
    List<User> list = slaveMapper.list(area);
    return list;
}

@DS("master")
public List<String> list(String code) {
    List<String> list = masterMapper.list(code);
    return list;
}

配置application.yml

dynamic:
  primary: master 
  datasource:
    master:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/master
      username: root
      password: 123456

    slave:
      url: jdbc:mysql://127.0.0.1:3306/slave
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver

是不是使用起来很方便!!!

3、特殊情况@DS("")不能满足我们的所有需求,需要手动切换;

比如我们在slave数据源的service方法中要使用master中的数据,数据在slave,字典在master中,于是如直接注解(这样写是错误的,并不能达到我们想要的效果)

@DS("slave")
public List<KeyValueVo> list(String areaCode) {
    List<KeyValueVo> list = slaveMapper.list(areaCode);
    List<Dict> educationes = listDict("education");
    ...后续操作...
}

@DS("master")
public List<Dict> listDict(String dictType) {
    List<Dict> list = dictMapper.list(dictType);
    return list;
}

原因: mybatis-plus数据源是利用spring-aop实现的,对于aop而言它是以每次请求为单位的,简单的说,虽然我们使用了两个方法,分别配置了两个@DS("),但其实第二个并不会生效

解决办法,对于一次请求两个数据源在把第二个数据源改为手动修改,下面的手动修改配置:

public List<Dict> listDict(String dictType) {
    DynamicDataSourceContextHolder.push("master");
    List<Dict> list = dictMapper.list(dictType);
    DynamicDataSourceContextHolder.poll();
    return list;
}




private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() {
    protected Object initialValue() {
        return new ArrayDeque();
    }
};

private DynamicDataSourceContextHolder() {
}

public static String peek() {
    return (String)((Deque)LOOKUP_KEY_HOLDER.get()).peek();
}

public static void push(String ds) {
    ((Deque)LOOKUP_KEY_HOLDER.get()).push(StringUtils.isEmpty(ds) ? "" : ds);
}

public static void poll() {
    Deque<String> deque = (Deque)LOOKUP_KEY_HOLDER.get();
    deque.poll();
    if (deque.isEmpty()) {
        LOOKUP_KEY_HOLDER.remove();
    }

}

DynamicDataSourceContextHolder中定义了一个双端队列LOOKUP_KEY_HOLDER,它的peek()方法每次取得时当前线程的首个,所以我们在手动却换的过程中,加入一个我们想要的datasource,然后通过**poll()**方法删除就能达到我们手动切换的目的。