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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 分库分表(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.在配置文件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框架源码说明)
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
11月前
|
Java 关系型数据库 中间件
分库分表(3)——ShardingJDBC实践
分库分表(3)——ShardingJDBC实践
233 0
分库分表(3)——ShardingJDBC实践
|
存储 SQL 开发框架
SpringCloud微服务实战——搭建企业级开发框架(二十七):集成多数据源+Seata分布式事务+读写分离+分库分表
读写分离:为了确保数据库产品的稳定性,很多数据库拥有双机热备功能。也就是,第一台数据库服务器,是对外提供增删改业务的生产服务器;第二台数据库服务器,主要进行读的操作。   目前有多种方式实现读写分离,一种是Mycat这种数据库中间件,需要单独部署服务,通过配置来实现读写分离,不侵入到业务代码中;还有一种是dynamic-datasource/shardingsphere-jdbc这种,需要在业务代码引入jar包进行开发。
982 26
SpringCloud微服务实战——搭建企业级开发框架(二十七):集成多数据源+Seata分布式事务+读写分离+分库分表
|
4月前
|
SQL 负载均衡 算法
使用ShardingJDBC实现分库分表
使用ShardingJDBC实现分库分表
|
11月前
|
SQL 算法 Java
分库分表(4)——ShardingJDBC原理和源码分析
分库分表(4)——ShardingJDBC原理和源码分析
380 1
分库分表(4)——ShardingJDBC原理和源码分析
|
SQL cobar 算法
SpringBoot 2 种方式快速实现分库分表,轻松拿捏!
SpringBoot 2 种方式快速实现分库分表,轻松拿捏!
4969 6
SpringBoot 2 种方式快速实现分库分表,轻松拿捏!
|
存储 算法 Java
从零玩转ShardingSphere分库分表 (概括)
从零玩转ShardingSphere分库分表 (概括)
91 0
|
算法 测试技术 Apache
分库分表实战
分库分表实战
236 0
|
存储 SQL 缓存
基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表
基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表
261 0
基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表
|
存储 SQL 运维
2、【ShardingSphere】做优化上来就分库分表?请慎重分库分表
读写分离,基本是目前商业开发最可靠的手段了。让我们有了更好的数据查询效率。最大的缺陷在于读写分离会增加MySQL服务器的预算。同时MySQL在高并发的情况下,slave也会有延迟,错误等。
272 0
|
SQL cobar 算法
ShardingJdbc分库分表浅谈
在传统的中小公司里面,尤其是以企业内部的办公系统、REP系统,或者体量不是很大的互联网公司里面,搭建一套单库和单表足以应对生产的业务数据量了。而在一些互联网大公司里面,单表每天有上100w的数据业务增量时,就要考虑分库分表的策略了。否则,无论是数据的存储、访问、更新等操作,单库和单表都会影响系统和数据库的性能。
327 0