一、概述

当把数据库进行分布分表等集群化部署后,在应用层就需要能够随时切换访问数据源,这就需要用到动态数据源的技术。应用是通过DataSource来访问数据库的,所以动态数据源实现的技术归根结底是在能够根据情况动态切换DataSource。

二、基于Spring的AbstractRoutingDataSource实现动态数据源

基于Spring提供的AbstractRoutingDataSource组件,实现快速切换后端访问的实际数据库,该类实质充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上。

源码分析

在AbstractRoutingDataSource的源码中,其继承AbstractDataSource抽象类,其核心方法为getConnection(),又可以发现getConnection()主要调用determineTargetDataSource()方法,该方法是确定使用哪个DataSource的核心,该部分逻辑调用determineCurrentLookupKey()抽象方法,所以我们只需要实现determineCurrentLookupKey()抽象方法即可实现动态切换数据源。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
  //...省略
      public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

    public Connection getConnection(String username, String password) throws SQLException {
        return this.determineTargetDataSource().getConnection(username, password);
    }
    //...省略
      protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }

        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else {
            return dataSource;
        }
    }

    @Nullable
    protected abstract Object determineCurrentLookupKey();
      //...省略
}

代码实现

1.在配置文件application.yml中配置多数据源信息。

server:
  port: 9000

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #自定义第一个数据源
    datasource1:
      url: jdbc:mysql://xxxx:3306/test?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: xxxx
      initial-size: 1
      min-idle: 1application.yml
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver
    #自定义第二个数据源
    datasource2:
      url: jdbc:mysql://xxxx:3306/test1?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: xxxx
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-borrow: true
      driver-class-name: com.mysql.cj.jdbc.Driver

创建数据源DataSource的配置类,其中创建2个 DataSource的Bean实例。

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.datasource1")
    public DataSource dataSource1() {
        // 底层会自动拿到spring.datasource1中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DataSource dataSource2() {
        // 底层会自动拿到spring.datasource2中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }
}

3.创建动态数据源DynamicDataSource的配置类。其中核心是实现determineCurrentLookupKey()方法,通过静态的ThreadLocal name变量,可以实现获取当前线程需要的数据源。

@Component("dynamicDataSource")
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {

    public static ThreadLocal<String> name = new ThreadLocal<>();
    @Override
    protected Object determineCurrentLookupKey() {
        return name.get();
    }

    @Resource(name = "dataSource1")
    DataSource dataSource1;
    @Resource(name = "dataSource2")
    DataSource dataSource2;

    @Override
    public void afterPropertiesSet() {
        // 为targetDataSources初始化所有数据源
        Map<Object, Object> targetDataSources=new HashMap<>();
        targetDataSources.put("ds1",dataSource1);
        targetDataSources.put("ds2",dataSource2);

        super.setTargetDataSources(targetDataSources);

        // 为defaultTargetDataSource 设置默认的数据源
        super.setDefaultTargetDataSource(dataSource1);

        super.afterPropertiesSet();
    }
}

4.接口测试动态数据源。

import com.yangnk.mybatisplusdemo.config.DynamicDataSource;
import com.yangnk.mybatisplusdemo.domain.UserInfo;
import com.yangnk.mybatisplusdemo.mapper.UserInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Random;

@Controller
@RequestMapping("/RDS")
public class MyRDSController {

    @Autowired
    UserInfoMapper userInfoMapper;

    @ResponseBody
    @RequestMapping("/add1")
    public String add1(@RequestParam(value = "dsKey",defaultValue = "ds1") String dsKey){
        DynamicDataSource.name.set(dsKey);
        System.out.println("add1");

        int nextInt = new Random().nextInt(100);
        UserInfo c = new UserInfo();
        c.setId(nextInt);
        c.setUser_name("name" + nextInt);
        c.setAge(nextInt);
        userInfoMapper.insert(c);
        System.out.println(c);
        DynamicDataSource.name.remove();
        return c.toString();
    }

    @ResponseBody
    @RequestMapping("/add2")
    public String add2(@RequestParam(value = "dsKey",defaultValue = "ds2") String dsKey){
        DynamicDataSource.name.set(dsKey);
        System.out.println("add2");
        int nextInt = new Random().nextInt(100) + 100;
        UserInfo c = new UserInfo();
        c.setId(nextInt);
        c.setUser_name("name" + nextInt);
        c.setAge(nextInt);
        userInfoMapper.insert(c);
        System.out.println(c);
        DynamicDataSource.name.remove();
        return c.toString();
    }
}

三、基于dynamic-datasource实现动态数据源

dynamic-datasource是一个能实现动态切换数据源的框架,相较于基于Spring的AbstractRoutingDataSource实现动态数据源,他还有其他非常丰富的功能。

特性

  1. 数据源分组,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  2. 内置敏感参数加密和启动初始化表结构schema数据库database。
  3. 提供对Druid,Mybatis-Plus,P6sy,Jndi的快速集成。
  4. 简化Druid和HikariCp配置,提供全局参数配置。
  5. 提供自定义数据源来源接口(默认使用yml或properties配置)。
  6. 提供项目启动后增减数据源方案。
  7. 提供Mybatis环境下的 纯读写分离 方案。
  8. 使用spel动态参数解析数据源,如从session,header或参数中获取数据源。(多租户架构神器)
  9. 提供多层数据源嵌套切换。(ServiceA >>> ServiceB >>> ServiceC,每个Service都是不同的数据源)
  10. 提供 不使用注解 而 使用 正则 或 spel 来切换数据源方案(实验性功能)。
  11. 基于seata的分布式事务支持。

代码实现

以下只展示重要步骤和重要信息。

1.POM配置文件添加dynamic-datasource-spring-boot-starter依赖项。

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

2.配置application.yml,在datasource配置项后添加多数据源。

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #使用dynamicDatasource框架
    dynamic:
      #设置默认的数据源或者数据源组,read
      primary: read
      #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      strict: false
      datasource:
        db1:
          url: jdbc:mysql://xxxx:3306/test?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
          username: root
          password: xxxx
          initial-size: 1
          min-idle: 1
          max-active: 20
          test-on-borrow: true
          driver-class-name: com.mysql.cj.jdbc.Driver
        db2:
          url: jdbc:mysql://xxxx:3306/test1?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
          username: root
          password: xxxx
          initial-size: 1
          min-idle: 1
          max-active: 20
          test-on-borrow: true
          driver-class-name: com.mysql.cj.jdbc.Driver
server:
  port: 9000

3.通过@DS(“xx”)注解选择需要的数据源datasource。

@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
    implements UserInfoService{

    @Autowired
    UserInfoMapper userInfoMapper;

    @Override
    @DS("db1")
    @Transactional
    public void insert1() {
        System.out.println("add1");
        int nextInt = new Random().nextInt(100);
        UserInfo c = new UserInfo();
        c.setId(nextInt);
        c.setUser_name("name" + nextInt);
        c.setAge(nextInt);
        userInfoMapper.insert(c);
        System.out.println(c);
    }

    @Override
    @DS("db2")
    @Transactional
    public void insert2() {
        System.out.println("add2");
        int nextInt = new Random().nextInt(100) + 100;
        UserInfo c = new UserInfo();
        c.setId(nextInt);
        c.setUser_name("name" + nextInt);
        c.setAge(nextInt);
        userInfoMapper.insert(c);
        System.out.println(c);
    }
}

参考资料

  1. 使用dynamic-datasource-spring-boot-starter做多数据源及源码分析:https://blog.csdn.net/w57685321/article/details/106823660 (有详细用dynamic-datasource框架源码说明)
10-10 21:19