• 分库分表(2)——动态数据源实践


    一、概述

    当把数据库进行分布分表等集群化部署后,在应用层就需要能够随时切换访问数据源,这就需要用到动态数据源的技术。应用是通过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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    代码实现

    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
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    创建数据源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();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

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

    @Component("dynamicDataSource")
    @Primary
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        public static ThreadLocal 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 targetDataSources=new HashMap<>();
            targetDataSources.put("ds1",dataSource1);
            targetDataSources.put("ds2",dataSource2);
    
            super.setTargetDataSources(targetDataSources);
    
            // 为defaultTargetDataSource 设置默认的数据源
            super.setDefaultTargetDataSource(dataSource1);
    
            super.afterPropertiesSet();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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();
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    三、基于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依赖项。

    
        com.baomidou
        dynamic-datasource-spring-boot-starter
        3.5.0
    
    • 1
    • 2
    • 3
    • 4

    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
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

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

    @Service
    public class UserInfoServiceImpl extends ServiceImpl
        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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    参考资料

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

      本文由博客一文多发平台 OpenWrite 发布!

  • 相关阅读:
    【C/C++】malloc/free 和 new/delete
    MediaPipe+OpenCV 实现实时手势识别(附Python源码)
    IntelliJ IDEA下基于Scala实现的Git检查工具
    JUC-ReentrantLock 源码
    [极客大挑战 2019]Http1
    构建工具 Vite、Webpack、Rollup对比
    论信息系统项目的安全管理(学习)
    跨平台使用:第三方美颜SDK在多种操作系统上的应用
    PyTorch官方文档学习笔记(备忘)
    STM32(1)跑马灯
  • 原文地址:https://blog.csdn.net/ynkimage/article/details/133757069