SpringBoot實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換的方法總結(jié)
簡介
項(xiàng)目開發(fā)中經(jīng)常會遇到多數(shù)據(jù)源同時使用的場景,比如冷熱數(shù)據(jù)的查詢等情況,我們可以使用類似現(xiàn)成的工具包來解決問題,但在多數(shù)據(jù)源的使用中通常伴隨著定制化的業(yè)務(wù),所以一般的公司還是會自行實(shí)現(xiàn)多數(shù)據(jù)源切換的功能,接下來一起使用實(shí)現(xiàn)自定義注解的形式來實(shí)現(xiàn)一下。
基礎(chǔ)配置
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驅(qū)動包 -->
<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>
管理數(shù)據(jù)源
我們應(yīng)用ThreadLocal來管理數(shù)據(jù)源信息,通過其中內(nèi)容的get,set,remove方法來獲取、設(shè)置、刪除當(dāng)前線程對應(yīng)的數(shù)據(jù)源。
/**
* ThreadLocal存放數(shù)據(jù)源變量
*
* @author 公眾號:程序員小富
* @date 2023/11/27 11:02
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* 獲取當(dāng)前線程的數(shù)據(jù)源
*
* @return 數(shù)據(jù)源名稱
*/
public static String getDataSource() {
return DATASOURCE_HOLDER.get();
}
/**
* 設(shè)置數(shù)據(jù)源
*
* @param dataSourceName 數(shù)據(jù)源名稱
*/
public static void setDataSource(String dataSourceName) {
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* 刪除當(dāng)前數(shù)據(jù)源
*/
public static void removeDataSource() {
DATASOURCE_HOLDER.remove();
}
}
重置數(shù)據(jù)源
創(chuàng)建 DynamicDataSource 類并繼承 AbstractRoutingDataSource,這樣我們就可以重置當(dāng)前的數(shù)據(jù)庫路由,實(shí)現(xiàn)切換成想要執(zhí)行的目標(biāo)數(shù)據(jù)庫。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
/**
* 重置當(dāng)前的數(shù)據(jù)庫路由,實(shí)現(xiàn)切換成想要執(zhí)行的目標(biāo)數(shù)據(jù)庫
*
* @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);
}
/**
* 這一步是關(guān)鍵,獲取注冊的數(shù)據(jù)源信息
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
配置數(shù)據(jù)庫
在 application.yml 中配置數(shù)據(jù)庫信息,使用dynamic_datasource_1、dynamic_datasource_2兩個數(shù)據(jù)庫
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
再將多個數(shù)據(jù)源注冊到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;
/**
* 注冊多個數(shù)據(jù)源
*
* @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<>();
// 設(shè)置默認(rèn)的數(shù)據(jù)源為Master
DataSource defaultDataSource = dynamicDatasourceMaster();
dataSourceMap.put("master", defaultDataSource);
dataSourceMap.put("slave", dynamicDatasourceSlave());
return new DynamicDataSource(defaultDataSource, dataSourceMap);
}
}
啟動類配置
在啟動類的@SpringBootApplication注解中排除DataSourceAutoConfiguration,否則會報(bào)錯。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
到這多數(shù)據(jù)源的基礎(chǔ)配置就結(jié)束了,接下來測試一下
測試切換
準(zhǔn)備SQL
創(chuàng)建兩個庫dynamic_datasource_1、dynamic_datasource_2,庫中均創(chuàng)建同一張表 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');
手動切換數(shù)據(jù)源
這里我準(zhǔn)備了一個接口來驗(yàn)證,傳入的 datasourceName 參數(shù)值就是剛剛注冊的數(shù)據(jù)源的key。
/**
* 動態(tài)數(shù)據(jù)源切換
*
* @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();
}
}
傳入?yún)?shù)master時:127.0.0.1:9004/switchDataSource/master

傳入?yún)?shù)slave時:127.0.0.1:9004/switchDataSource/slave

通過執(zhí)行結(jié)果,我們看到傳遞不同的數(shù)據(jù)源名稱,已經(jīng)實(shí)現(xiàn)了查詢對應(yīng)的數(shù)據(jù)庫數(shù)據(jù)。
注解切換數(shù)據(jù)源
上邊已經(jīng)成功實(shí)現(xiàn)了手動切換數(shù)據(jù)源,但這種方式頂多算是半自動,下邊我們來使用注解方式實(shí)現(xiàn)動態(tài)切換。
定義注解
我們先定一個名為DS的注解,作用域?yàn)镸ETHOD方法上,由于@DS中設(shè)置的默認(rèn)值是:master,因此在調(diào)用主數(shù)據(jù)源時,可以不用進(jìn)行傳值。
/**
* 定于數(shù)據(jù)源切換注解
*
* @author 公眾號:程序員小富
* @date 2023/11/27 11:02
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DS {
// 默認(rèn)數(shù)據(jù)源master
String value() default "master";
}
實(shí)現(xiàn)AOP
定義了@DS注解后,緊接著實(shí)現(xiàn)注解的AOP邏輯,拿到注解傳遞值,然后設(shè)置當(dāng)前線程的數(shù)據(jù)源
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;
/**
* 實(shí)現(xiàn)@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注解標(biāo)注,使用不同的數(shù)據(jù)源名稱,內(nèi)部執(zhí)行相同的查詢條件,看看結(jié)果如何?
@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();
}

通過執(zhí)行結(jié)果,看到通過應(yīng)用@DS注解也成功的進(jìn)行了數(shù)據(jù)源的切換。
事務(wù)管理
在動態(tài)切換數(shù)據(jù)源的時候有一個問題是要考慮的,那就是事務(wù)管理是否還會生效呢?
我們做個測試,新增一個接口分別插入兩條記錄,其中在插入第二條數(shù)據(jù)時將值設(shè)置超過了字段長度限制,會產(chǎn)生Data too long for column異常。
/**
* 驗(yàn)證一下事物控制
*/
// @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);
}
經(jīng)過測試發(fā)現(xiàn)執(zhí)行結(jié)果如下,即便實(shí)現(xiàn)動態(tài)切換數(shù)據(jù)源,本地事務(wù)依然可以生效。
- 不加上
@Transactional注解第一條記錄可以插入,第二條插入失敗 - 加上
@Transactional注解兩條記錄都不會插入成功
本文案例地址:
以上就是SpringBoot實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換的方法總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot動態(tài)數(shù)據(jù)源切換的資料請關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot中動態(tài)數(shù)據(jù)源配置與使用詳解
- SpringBoot配置動態(tài)數(shù)據(jù)源的實(shí)戰(zhàn)詳解
- SpringBoot自定義動態(tài)數(shù)據(jù)源的流程步驟
- SpringBoot實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換的項(xiàng)目實(shí)踐
- SpringBoot動態(tài)數(shù)據(jù)源連接測試的操作詳解
- springboot配置多數(shù)據(jù)源(靜態(tài)和動態(tài)數(shù)據(jù)源)
- SpringBoot中動態(tài)數(shù)據(jù)源是實(shí)現(xiàn)與用途
- springboot 動態(tài)數(shù)據(jù)源的實(shí)現(xiàn)方法(Mybatis+Druid)
- springboot動態(tài)數(shù)據(jù)源+分布式事務(wù)的實(shí)現(xiàn)
相關(guān)文章
Mybatis框架之代理模式(Proxy Pattern)的實(shí)現(xiàn)
本文主要介紹了MyBatis框架中使用代理模式ProxyPattern的原理和實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-11-11
SpringBoot整合atomikos實(shí)現(xiàn)跨庫事務(wù)的詳細(xì)方案
這篇文章主要介紹了SpringBoot整合atomikos實(shí)現(xiàn)跨庫事務(wù),業(yè)務(wù)主要涉及政府及企業(yè)且并發(fā)量不大,所以采用XA事務(wù),雖然性能有所損失,但是可以保證數(shù)據(jù)的強(qiáng)一致性,需要的朋友可以參考下2022-06-06
Sa-Token中的SaSession對象使用學(xué)習(xí)示例詳解
這篇文章主要為大家介紹了Sa-Token中的SaSession對象使用學(xué)習(xí)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
java實(shí)現(xiàn)微信支付結(jié)果通知
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)微信支付結(jié)果通知,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01
Spring boot如何基于攔截器實(shí)現(xiàn)訪問權(quán)限限制
這篇文章主要介紹了Spring boot如何基于攔截器實(shí)現(xiàn)訪問權(quán)限限制,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10

