• SpringBoot 这么实现动态数据源切换,就很丝滑!


    大家好,我是小富~

    简介

    项目开发中经常会遇到多数据源同时使用的场景,比如冷热数据的查询等情况,我们可以使用类似现成的工具包来解决问题,但在多数据源的使用中通常伴随着定制化的业务,所以一般的公司还是会自行实现多数据源切换的功能,接下来一起使用实现自定义注解的形式来实现一下。

    基础配置

    yml配置

    pom.xml文件引入必要的Jar

    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>2.7.6version>
    parent>
    <groupId>com.dynamicgroupId>
    <artifactId>springboot-dynamic-datasourceartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <properties>
    <maven.compiler.source>8maven.compiler.source>
    <maven.compiler.target>8maven.compiler.target>
    <mybatis.plus.version>3.5.3.1mybatis.plus.version>
    <mysql.connector.version>8.0.32mysql.connector.version>
    <druid.version>1.2.6druid.version>
    properties>
    <dependencies>
    <dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starterartifactId>
    dependency>
    <dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
    <groupId>com.mysqlgroupId>
    <artifactId>mysql-connector-jartifactId>
    <version>${mysql.connector.version}version>
    dependency>
    <dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
    <optional>trueoptional>
    dependency>
    <dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>${mybatis.plus.version}version>
    dependency>
    <dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druid-spring-boot-starterartifactId>
    <version>${druid.version}version>
    dependency>
    <dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-lang3artifactId>
    <version>3.7version>
    dependency>
    <dependency>
    <groupId>org.aspectjgroupId>
    <artifactId>aspectjweaverartifactId>
    dependency>
    dependencies>
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-maven-pluginartifactId>
    plugin>
    plugins>
    build>
    project>

    管理数据源

    我们应用ThreadLocal来管理数据源信息,通过其中内容的get,set,remove方法来获取、设置、删除当前线程对应的数据源。

    /**
    * ThreadLocal存放数据源变量
    *
    * @author 公众号:程序员小富
    * @date 2023/11/27 11:02
    */
    public class DataSourceContextHolder {
    private static final ThreadLocal DATASOURCE_HOLDER = new ThreadLocal<>();
    /**
    * 获取当前线程的数据源
    *
    * @return 数据源名称
    */
    public static String getDataSource() {
    return DATASOURCE_HOLDER.get();
    }
    /**
    * 设置数据源
    *
    * @param dataSourceName 数据源名称
    */
    public static void setDataSource(String dataSourceName) {
    DATASOURCE_HOLDER.set(dataSourceName);
    }
    /**
    * 删除当前数据源
    */
    public static void removeDataSource() {
    DATASOURCE_HOLDER.remove();
    }
    }

    重置数据源

    创建 DynamicDataSource 类并继承 AbstractRoutingDataSource,这样我们就可以重置当前的数据库路由,实现切换成想要执行的目标数据库。

    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import javax.sql.DataSource;
    import java.util.Map;
    /**
    * 重置当前的数据库路由,实现切换成想要执行的目标数据库
    *
    * @author 公众号:程序员小富
    * @date 2023/11/27 11:02
    */
    public class DynamicDataSource extends AbstractRoutingDataSource {
    public DynamicDataSource(DataSource defaultDataSource, Map targetDataSources) {
    super.setDefaultTargetDataSource(defaultDataSource);
    super.setTargetDataSources(targetDataSources);
    }
    /**
    * 这一步是关键,获取注册的数据源信息
    * @return
    */
    @Override
    protected Object determineCurrentLookupKey() {
    return DataSourceContextHolder.getDataSource();
    }
    }

    配置数据库

    在 application.yml 中配置数据库信息,使用dynamic_datasource_1dynamic_datasource_2两个数据库

    spring:
    datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
    master:
    url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 12345
    driver-class-name: com.mysql.cj.jdbc.Driver
    slave:
    url: jdbc:mysql://127.0.0.1:3306/dynamic_datasource_2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 12345
    driver-class-name: com.mysql.cj.jdbc.Driver

    再将多个数据源注册到DataSource.

    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 org.springframework.context.annotation.Primary;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    /**
    * 注册多个数据源
    *
    * @author 公众号:程序员小富
    * @date 2023/11/27 11:02
    */
    @Configuration
    public class DateSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource dynamicDatasourceMaster() {
    return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource dynamicDatasourceSlave() {
    return DruidDataSourceBuilder.create().build();
    }
    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource createDynamicDataSource() {
    Map dataSourceMap = new HashMap<>();
    // 设置默认的数据源为Master
    DataSource defaultDataSource = dynamicDatasourceMaster();
    dataSourceMap.put("master", defaultDataSource);
    dataSourceMap.put("slave", dynamicDatasourceSlave());
    return new DynamicDataSource(defaultDataSource, dataSourceMap);
    }
    }

    启动类配置

    在启动类的@SpringBootApplication注解中排除DataSourceAutoConfiguration,否则会报错。

    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

    到这多数据源的基础配置就结束了,接下来测试一下

    测试切换

    准备SQL

    创建两个库dynamic_datasource_1、dynamic_datasource_2,库中均创建同一张表 t_dynamic_datasource_data。

    CREATE TABLE `t_dynamic_datasource_data` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `source_name` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`)
    );

    dynamic_datasource_1.t_dynamic_datasource_data表中插入

    insert into t_dynamic_datasource_data (source_name) value ('dynamic_datasource_master');

    dynamic_datasource_2.t_dynamic_datasource_data表中插入

    insert into t_dynamic_datasource_data (source_name) value ('dynamic_datasource_slave');

    手动切换数据源

    这里我准备了一个接口来验证,传入的 datasourceName 参数值就是刚刚注册的数据源的key。

    /**
    * 动态数据源切换
    *
    * @author 公众号:程序员小富
    * @date 2023/11/27 11:02
    */
    @RestController
    public class DynamicSwitchController {
    @Resource
    private DynamicDatasourceDataMapper dynamicDatasourceDataMapper;
    @GetMapping("/switchDataSource/{datasourceName}")
    public String switchDataSource(@PathVariable("datasourceName") String datasourceName) {
    DataSourceContextHolder.setDataSource(datasourceName);
    DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
    DataSourceContextHolder.removeDataSource();
    return dynamicDatasourceData.getSourceName();
    }
    }

    传入参数master时:127.0.0.1:9004/switchDataSource/master

    传入参数slave时:127.0.0.1:9004/switchDataSource/slave

    通过执行结果,我们看到传递不同的数据源名称,已经实现了查询对应的数据库数据。

    注解切换数据源

    上边已经成功实现了手动切换数据源,但这种方式顶多算是半自动,下边我们来使用注解方式实现动态切换。

    定义注解

    我们先定一个名为DS的注解,作用域为METHOD方法上,由于@DS中设置的默认值是:master,因此在调用主数据源时,可以不用进行传值。

    /**
    * 定于数据源切换注解
    *
    * @author 公众号:程序员小富
    * @date 2023/11/27 11:02
    */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface DS {
    // 默认数据源master
    String value() default "master";
    }

    实现AOP

    定义了@DS注解后,紧接着实现注解的AOP逻辑,拿到注解传递值,然后设置当前线程的数据源

    import com.dynamic.config.DataSourceContextHolder;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    import java.lang.reflect.Method;
    import java.util.Objects;
    /**
    * 实现@DS注解的AOP切面
    *
    * @author 公众号:程序员小富
    * @date 2023/11/27 11:02
    */
    @Aspect
    @Component
    @Slf4j
    public class DSAspect {
    @Pointcut("@annotation(com.dynamic.aspect.DS)")
    public void dynamicDataSource() {
    }
    @Around("dynamicDataSource()")
    public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
    MethodSignature signature = (MethodSignature) point.getSignature();
    Method method = signature.getMethod();
    DS ds = method.getAnnotation(DS.class);
    if (Objects.nonNull(ds)) {
    DataSourceContextHolder.setDataSource(ds.value());
    }
    try {
    return point.proceed();
    } finally {
    DataSourceContextHolder.removeDataSource();
    }
    }
    }

    测试注解

    再添加两个接口测试,使用@DS注解标注,使用不同的数据源名称,内部执行相同的查询条件,看看结果如何?

    @DS(value = "master")
    @GetMapping("/dbMaster")
    public String dbMaster() {
    DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
    return dynamicDatasourceData.getSourceName();
    }

    @DS(value = "slave")
    @GetMapping("/dbSlave")
    public String dbSlave() {
    DynamicDatasourceData dynamicDatasourceData = dynamicDatasourceDataMapper.selectOne(null);
    return dynamicDatasourceData.getSourceName();
    }

    通过执行结果,看到通过应用@DS注解也成功的进行了数据源的切换。

    事务管理

    在动态切换数据源的时候有一个问题是要考虑的,那就是事务管理是否还会生效呢?

    我们做个测试,新增一个接口分别插入两条记录,其中在插入第二条数据时将值设置超过了字段长度限制,会产生Data too long for column异常。

    /**
    * 验证一下事物控制
    */
    // @Transactional(rollbackFor = Exception.class)
    @DS(value = "slave")
    @GetMapping("/dbTestTransactional")
    public void dbTestTransactional() {
    DynamicDatasourceData datasourceData = new DynamicDatasourceData();
    datasourceData.setSourceName("test");
    dynamicDatasourceDataMapper.insert(datasourceData);
    DynamicDatasourceData datasourceData1 = new DynamicDatasourceData();
    datasourceData1.setSourceName("testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest");
    dynamicDatasourceDataMapper.insert(datasourceData1);
    }

    经过测试发现执行结果如下,即便实现动态切换数据源,本地事务依然可以生效。

    • 不加上@Transactional注解第一条记录可以插入,第二条插入失败

    • 加上@Transactional注解两条记录都不会插入成功

    本文案例地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot101/通用功能/springboot-config-order

  • 相关阅读:
    【软件与系统安全笔记】四、内存破坏漏洞
    十二、集合(5)
    事件过滤器
    Mathorcup数学建模竞赛第四届-【妈妈杯】C题:家庭暑假旅游套餐的设计(附MATLAB代码)
    Cesium
    Linux的各种环境配置
    iOS App上架全流程及相关处理
    电脑如何查找重复文件?轻松揪出它!
    E中国集装箱涂料行业竞争态势及投资盈利预测报告2022-2028年
    【CentOS】忘记root密码
  • 原文地址:https://www.cnblogs.com/chengxy-nds/p/17926002.html