SpringBoot 這麼實現動態數據源切換,就很絲滑!

大家好,我是小富~

簡介

項目開發中經常會遇到多數據源同時使用的場景,比如冷熱數據的查詢等情況,我們可以使用類似現成的工具包來解決問題,但在多數據源的使用中通常伴隨着定製化的業務,所以一般的公司還是會自行實現多數據源切換的功能,接下來一起使用實現自定義註解的形式來實現一下。

基礎配置

yml配置

pom.xml文件引入必要的Jar

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
    </parent>
    <groupId>com.dynamic</groupId>
    <artifactId>springboot-dynamic-datasource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <mybatis.plus.version>3.5.3.1</mybatis.plus.version>
        <mysql.connector.version>8.0.32</mysql.connector.version>
        <druid.version>1.2.6</druid.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- springboot核心包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mysql驅動包 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql.connector.version}</version>
        </dependency>
        <!-- lombok工具包 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis.plus.version}</version>
        </dependency>
        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

管理數據源

我們應用ThreadLocal來管理數據源信息,通過其中內容的get,set,remove方法來獲取、設置、刪除當前線程對應的數據源。

/**
 * ThreadLocal存放數據源變量
 *
 * @author 公衆號:程序員小富
 * @date 2023/11/27 11:02
 */
public class DataSourceContextHolder {

    private static final ThreadLocal<String> 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<Object, Object> 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<Object, Object> 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章