SpringBoot中實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換過程
隨著業(yè)務(wù)的不斷發(fā)展和系統(tǒng)的日益復(fù)雜,傳統(tǒng)的單一數(shù)據(jù)庫架構(gòu)往往難以滿足高并發(fā)、海量數(shù)據(jù)以及高可用的需求。為了應(yīng)對這些挑戰(zhàn),分布式數(shù)據(jù)庫架構(gòu)逐漸成為主流。而在分布式數(shù)據(jù)庫架構(gòu)中,如何靈活高效地切換數(shù)據(jù)源,成為了一個關(guān)鍵問題。

這個問題在多租戶架構(gòu)、讀寫分離、數(shù)據(jù)庫分片等場景中尤為重要。通過動態(tài)數(shù)據(jù)源切換,我們能夠根據(jù)不同的業(yè)務(wù)需求、請求來源或者負(fù)載情況動態(tài)選擇不同的數(shù)據(jù)源,從而提升系統(tǒng)的性能、可擴(kuò)展性和高可用性。
基礎(chǔ)知識
Spring Boot 中實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換 之前,我們需要先了解一些相關(guān)的基礎(chǔ)知識,這些知識將幫助我們理解動態(tài)數(shù)據(jù)源切換的概念和實(shí)施方法。

1. 什么是數(shù)據(jù)源?
數(shù)據(jù)源(DataSource)是一個接口,它為 Java 程序提供數(shù)據(jù)庫連接。
它通常由數(shù)據(jù)庫連接池實(shí)現(xiàn),比如 HikariCP、C3P0 或 DBCP 等,數(shù)據(jù)源的作用就是幫助應(yīng)用程序與數(shù)據(jù)庫建立連接并管理這些連接。
2. 動態(tài)數(shù)據(jù)源切換的概念
動態(tài)數(shù)據(jù)源切換的核心思想是,根據(jù)不同的業(yè)務(wù)場景、請求類型、用戶身份或者負(fù)載情況,動態(tài)地切換到不同的數(shù)據(jù)源。這種技術(shù)在多租戶架構(gòu)、讀寫分離、數(shù)據(jù)庫分片等場景中尤為重要。
- 多租戶架構(gòu):每個租戶可以使用獨(dú)立的數(shù)據(jù)庫,或者共享一個數(shù)據(jù)庫中的表,但不同的租戶數(shù)據(jù)是隔離的。
- 讀寫分離:在高并發(fā)的系統(tǒng)中,通常采用主從數(shù)據(jù)庫架構(gòu),主庫用于寫操作,從庫用于讀操作。動態(tài)切換數(shù)據(jù)源可以根據(jù)請求的類型(讀/寫)自動選擇合適的數(shù)據(jù)源。
- 數(shù)據(jù)庫分片:當(dāng)單個數(shù)據(jù)庫承載的數(shù)據(jù)量過大時,可以將數(shù)據(jù)分散到多個數(shù)據(jù)庫(分片),根據(jù)請求的數(shù)據(jù)范圍選擇不同的數(shù)據(jù)庫。
3. Spring Boot 中的默認(rèn)數(shù)據(jù)源配置
在 Spring Boot 中,默認(rèn)情況下,我們通過 application.properties 或 application.yml 文件配置數(shù)據(jù)源。Spring Boot 會自動根據(jù)這些配置創(chuàng)建一個單一的數(shù)據(jù)源對象,應(yīng)用程序使用該數(shù)據(jù)源進(jìn)行數(shù)據(jù)庫操作。
例如,常見的單數(shù)據(jù)源配置如下:
spring.datasource.url=jdbc:mysql://localhost:3306/mydb spring.datasource.username=root spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
4. 動態(tài)數(shù)據(jù)源的挑戰(zhàn)
在一些場景下,我們可能需要基于某些條件切換數(shù)據(jù)源,比如:
- ● 根據(jù)用戶身份切換到不同的數(shù)據(jù)庫。
- ● 根據(jù)請求的類型(如讀請求或?qū)懻埱螅┣袚Q到不同的數(shù)據(jù)源。
- ?在高并發(fā)情況下,動態(tài)地根據(jù)負(fù)載選擇數(shù)據(jù)庫。
在這種情況下,動態(tài)數(shù)據(jù)源切換將面臨如下挑戰(zhàn):
- ?● 事務(wù)的隔離性:在切換數(shù)據(jù)源時,必須確保事務(wù)的原子性和一致性,避免因切換數(shù)據(jù)源導(dǎo)致事務(wù)出錯或失敗。
- ● 數(shù)據(jù)源的管理:隨著數(shù)據(jù)源數(shù)量的增加,如何管理和切換多個數(shù)據(jù)源,確保高效和穩(wěn)定的連接池管理,成為一個問題。
- ● 性能問題:頻繁的切換數(shù)據(jù)源可能會導(dǎo)致性能瓶頸,因此需要優(yōu)化數(shù)據(jù)源的切換策略,確保系統(tǒng)的高效性。
?
5. Spring 中的數(shù)據(jù)源切換方式
在 Spring 框架中,常見的數(shù)據(jù)源切換方式有兩種:
- 基于動態(tài)代理的方式:通過 AOP(面向切面編程)技術(shù),結(jié)合 Spring 的 AbstractRoutingDataSource 實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換。這種方法通過創(chuàng)建一個動態(tài)的代理類,根據(jù)業(yè)務(wù)邏輯動態(tài)決定當(dāng)前使用的數(shù)據(jù)源。
- 基于注解的方式:通過自定義注解和 AOP 切面結(jié)合,標(biāo)識需要切換數(shù)據(jù)源的方法或類,在方法調(diào)用時動態(tài)切換數(shù)據(jù)源。
設(shè)計思路
設(shè)計 Spring Boot 中動態(tài)數(shù)據(jù)源切換 的思路時,需要結(jié)合應(yīng)用場景和系統(tǒng)需求來選擇合適的架構(gòu)和技術(shù)。

1. 明確應(yīng)用場景
首先,我們需要明確為什么要實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換。常見的場景包括:
- ?多租戶架構(gòu):每個租戶可能使用不同的數(shù)據(jù)庫,或者同一數(shù)據(jù)庫中使用不同的 schema 進(jìn)行隔離。
- ?讀寫分離:將寫操作指向主庫,讀操作指向從庫,從而提高系統(tǒng)的并發(fā)處理能力。
- ?數(shù)據(jù)庫分片:數(shù)據(jù)量過大,單一數(shù)據(jù)庫無法滿足需求時,通過分片將數(shù)據(jù)分布到不同的數(shù)據(jù)庫節(jié)點(diǎn)。
2. 選擇數(shù)據(jù)源切換策略
根據(jù)不同的場景,數(shù)據(jù)源切換的策略也有所不同。通常有以下幾種策略:
- ?按請求類型切換:根據(jù)請求類型(如讀請求或?qū)懻埱螅﹦討B(tài)選擇數(shù)據(jù)源。寫操作使用主庫,讀操作使用從庫。
- ?按用戶切換:根據(jù)當(dāng)前用戶的身份或租戶信息,動態(tài)選擇數(shù)據(jù)源。例如,多租戶場景下,不同租戶可能使用不同的數(shù)據(jù)庫。
- ?按業(yè)務(wù)邏輯切換:根據(jù)具體的業(yè)務(wù)邏輯來選擇數(shù)據(jù)源。比如,特定業(yè)務(wù)流程可能需要訪問某個特定的數(shù)據(jù)庫。
3. 數(shù)據(jù)源管理
在設(shè)計動態(tài)數(shù)據(jù)源切換時,數(shù)據(jù)源的管理非常重要。需要解決以下問題:
- ?連接池的管理:每個數(shù)據(jù)源都應(yīng)該有獨(dú)立的數(shù)據(jù)庫連接池。可以使用 HikariCP 等數(shù)據(jù)庫連接池來管理連接的生命周期和性能。
- ?數(shù)據(jù)源的動態(tài)切換:實(shí)現(xiàn)動態(tài)切換數(shù)據(jù)源的核心是
AbstractRoutingDataSource。通過AbstractRoutingDataSource,可以在運(yùn)行時根據(jù)某些條件動態(tài)選擇數(shù)據(jù)源。 - ?事務(wù)的一致性:在動態(tài)切換數(shù)據(jù)源時,需要確保事務(wù)的一致性。Spring 提供了
@Transactional注解來保證事務(wù)的一致性,但在多數(shù)據(jù)源環(huán)境下,必須確保事務(wù)管理能夠跨多個數(shù)據(jù)源正確執(zhí)行。
4. 動態(tài)數(shù)據(jù)源切換的實(shí)現(xiàn)方案
實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換可以通過以下步驟:
4.1 創(chuàng)建動態(tài)數(shù)據(jù)源路由
AbstractRoutingDataSource 是 Spring 提供的一個抽象類,可以通過它實(shí)現(xiàn)數(shù)據(jù)源的動態(tài)路由。我們可以繼承 AbstractRoutingDataSource,并重寫 determineCurrentLookupKey() 方法,該方法用于獲取當(dāng)前應(yīng)該使用的數(shù)據(jù)源的標(biāo)識?;谠摌?biāo)識,系統(tǒng)可以在多個數(shù)據(jù)源之間進(jìn)行切換。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType(); // 獲取當(dāng)前的數(shù)據(jù)源標(biāo)識
}
}
4.2 數(shù)據(jù)源上下文管理
為了在運(yùn)行時動態(tài)切換數(shù)據(jù)源,我們需要維護(hù)一個上下文來存儲當(dāng)前的數(shù)據(jù)庫標(biāo)識。通常,可以通過 ThreadLocal 來管理當(dāng)前線程的數(shù)據(jù)庫標(biāo)識??梢允褂?DataSourceContextHolder 來進(jìn)行數(shù)據(jù)源的切換。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
4.3 切換數(shù)據(jù)源
在具體的業(yè)務(wù)邏輯中,可以通過設(shè)置 DataSourceContextHolder 來切換數(shù)據(jù)源。例如:
public class SomeService {
public void someMethod() {
DataSourceContextHolder.setDataSourceType("master"); // 切換到主庫
// 執(zhí)行主庫操作
}
public void anotherMethod() {
DataSourceContextHolder.setDataSourceType("slave"); // 切換到從庫
// 執(zhí)行從庫操作
}
}
4.4 配置多個數(shù)據(jù)源
在 application.yml 或 application.properties 配置文件中,可以配置多個數(shù)據(jù)源。Spring Boot 會自動為每個數(shù)據(jù)源生成一個 DataSource Bean。我們需要為每個數(shù)據(jù)源配置合適的連接池,并根據(jù)需要進(jìn)行切換。
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/master
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/slave
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver4.5 事務(wù)管理
在多數(shù)據(jù)源的情況下,事務(wù)的管理需要特別注意。如果使用的是 @Transactional 注解,Spring 會根據(jù)當(dāng)前的數(shù)據(jù)源來處理事務(wù)。為保證事務(wù)一致性,可以使用 PlatformTransactionManager 來管理不同數(shù)據(jù)源的事務(wù)。
5. 優(yōu)雅的切換和性能優(yōu)化
動態(tài)切換數(shù)據(jù)源的性能和穩(wěn)定性是設(shè)計的關(guān)鍵。我們可以采取以下策略來優(yōu)化性能:
- ●數(shù)據(jù)源的連接池管理:確保每個數(shù)據(jù)源都配置了合適的連接池,避免頻繁創(chuàng)建和銷毀連接。
- ●懶加載數(shù)據(jù)源:只有在需要的時候才初始化數(shù)據(jù)源,避免不必要的資源占用。
- ●數(shù)據(jù)源切換的粒度控制:避免頻繁切換數(shù)據(jù)源,盡量減少切換的粒度,降低系統(tǒng)的開銷。
6. 監(jiān)控與日志
為了更好地管理動態(tài)數(shù)據(jù)源切換,我們需要加入監(jiān)控和日志記錄機(jī)制,跟蹤哪些數(shù)據(jù)源被切換,當(dāng)前的數(shù)據(jù)源是什么,以及執(zhí)行的 SQL 查詢是什么。這有助于在問題出現(xiàn)時進(jìn)行排查。
實(shí)現(xiàn)步驟
實(shí)現(xiàn) Spring Boot 中動態(tài)數(shù)據(jù)源切換 的步驟涉及多個關(guān)鍵環(huán)節(jié),包括數(shù)據(jù)源的配置、動態(tài)數(shù)據(jù)源的切換、事務(wù)管理等。
1. 添加依賴
首先,確保你的 Spring Boot 項(xiàng)目中已經(jīng)引入了 Spring Data JPA 或 MyBatis 等數(shù)據(jù)訪問框架的依賴。然后,添加數(shù)據(jù)庫連接池和 Spring Boot Starter 數(shù)據(jù)源的依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>2. 配置多個數(shù)據(jù)源
在 application.yml 或 application.properties 中配置多個數(shù)據(jù)源。這里我們以主庫和從庫為例進(jìn)行配置。
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/master
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/slave
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver3. 創(chuàng)建動態(tài)數(shù)據(jù)源路由類
我們需要繼承 AbstractRoutingDataSource 類來實(shí)現(xiàn)數(shù)據(jù)源的動態(tài)路由。在該類中,通過 determineCurrentLookupKey() 方法來決定當(dāng)前使用哪個數(shù)據(jù)源。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType(); // 獲取當(dāng)前線程的數(shù)據(jù)源類型
}
}
4. 創(chuàng)建數(shù)據(jù)源上下文管理類
為了在不同的業(yè)務(wù)邏輯中切換數(shù)據(jù)源,我們需要創(chuàng)建一個上下文管理類,來管理當(dāng)前線程的 DataSource??梢允褂?ThreadLocal 來存儲當(dāng)前線程的數(shù)據(jù)源標(biāo)識。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 設(shè)置當(dāng)前線程使用的數(shù)據(jù)源
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
// 獲取當(dāng)前線程的數(shù)據(jù)源
public static String getDataSourceType() {
return contextHolder.get();
}
// 清除當(dāng)前線程的數(shù)據(jù)源
public static void clearDataSourceType() {
contextHolder.remove();
}
}
5. 配置數(shù)據(jù)源的 Bean
接下來,我們需要在 Spring 配置類中注冊多個數(shù)據(jù)源并設(shè)置動態(tài)數(shù)據(jù)源。在這個步驟中,動態(tài)數(shù)據(jù)源會根據(jù)上下文管理類返回的數(shù)據(jù)源類型來選擇具體的數(shù)據(jù)源。
@Configuration
@EnableTransactionManagement
public class DataSourceConfig {
@Bean
@Primary
public DataSource dataSource() {
DynamicDataSource dataSource = new DynamicDataSource();
// 配置主庫和從庫的數(shù)據(jù)源
dataSource.setTargetDataSources(getDataSources()); // 設(shè)置數(shù)據(jù)源
dataSource.setDefaultTargetDataSource(masterDataSource()); // 設(shè)置默認(rèn)數(shù)據(jù)源
return dataSource;
}
// 主庫的數(shù)據(jù)源
@Bean
public DataSource masterDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/master");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
// 從庫的數(shù)據(jù)源
@Bean
public DataSource slaveDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/slave");
dataSource.setUsername("root");
dataSource.setPassword("password");
return dataSource;
}
// 返回數(shù)據(jù)源的 map,Key 是數(shù)據(jù)源標(biāo)識(例如:主庫或從庫)
private Map<Object, Object> getDataSources() {
Map<Object, Object> dataSources = new HashMap<>();
dataSources.put("master", masterDataSource());
dataSources.put("slave", slaveDataSource());
return dataSources;
}
}
6. 在業(yè)務(wù)邏輯中切換數(shù)據(jù)源
在業(yè)務(wù)方法中,我們可以通過調(diào)用 DataSourceContextHolder.setDataSourceType() 來切換數(shù)據(jù)源。這里假設(shè)我們要根據(jù)業(yè)務(wù)邏輯切換數(shù)據(jù)源,比如主庫和從庫的讀寫分離。
@Service
public class SomeService {
@Transactional
public void writeData() {
// 切換到主庫進(jìn)行寫操作
DataSourceContextHolder.setDataSourceType("master");
// 執(zhí)行寫操作
}
@Transactional
public void readData() {
// 切換到從庫進(jìn)行讀操作
DataSourceContextHolder.setDataSourceType("slave");
// 執(zhí)行讀操作
}
}
7. 清理數(shù)據(jù)源上下文
在每次操作完成后,要確保清除當(dāng)前線程的數(shù)據(jù)源標(biāo)識,以避免影響后續(xù)的請求。
public class DataSourceAspect {
@After("@annotation(org.springframework.transaction.annotation.Transactional)")
public void clearDataSource() {
DataSourceContextHolder.clearDataSourceType();
}
}
8. 事務(wù)管理
在多數(shù)據(jù)源的情況下,事務(wù)管理需要特別小心。Spring 提供的 @Transactional 注解支持跨多個數(shù)據(jù)源進(jìn)行事務(wù)管理,但需要確保不同數(shù)據(jù)源的事務(wù)管理器是獨(dú)立的。通常,使用 PlatformTransactionManager 來確保每個數(shù)據(jù)源的事務(wù)能夠獨(dú)立處理。
9. 測試與優(yōu)化
完成以上步驟后,進(jìn)行數(shù)據(jù)源切換的測試,確保數(shù)據(jù)源能夠根據(jù)業(yè)務(wù)邏輯正確切換。在高并發(fā)場景下,還需要優(yōu)化連接池的配置,避免連接池資源浪費(fèi)。
切換策略
在 Spring Boot 中實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換時,切換策略是一個非常重要的環(huán)節(jié)。切換策略決定了何時以及如何從一個數(shù)據(jù)源切換到另一個數(shù)據(jù)源。
1. 讀寫分離策略
讀寫分離是最常見的動態(tài)數(shù)據(jù)源切換策略之一。主庫用于寫操作,從庫用于讀操作。數(shù)據(jù)源切換的策略通?;诓僮黝愋停?/p>
- ● 寫操作(Insert/Update/Delete):所有寫操作需要訪問主庫。
- ● 讀操作(Select):所有讀操作可以訪問從庫。
在進(jìn)行數(shù)據(jù)操作時,判斷當(dāng)前操作是讀操作還是寫操作,然后根據(jù)操作類型動態(tài)切換數(shù)據(jù)源。
@Service
public class DataService {
// 讀操作
public void readData() {
DataSourceContextHolder.setDataSourceType("slave"); // 切換到從庫
// 執(zhí)行讀操作
}
// 寫操作
public void writeData() {
DataSourceContextHolder.setDataSourceType("master"); // 切換到主庫
// 執(zhí)行寫操作
}
}
2. 基于線程或上下文的切換策略
此策略適用于每個線程有不同的數(shù)據(jù)庫訪問需求,例如基于用戶或請求的上下文來決定使用哪個數(shù)據(jù)源。使用 ThreadLocal 或 RequestContext 來存儲當(dāng)前請求的數(shù)據(jù)源信息,確保每個請求可以獨(dú)立切換數(shù)據(jù)源。通過上下文保存當(dāng)前請求的數(shù)據(jù)源,常見的做法是使用 ThreadLocal 來存儲每個線程的數(shù)據(jù)源信息。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
在請求中根據(jù)需要切換數(shù)據(jù)源:
public void processRequest(String dataSourceType) {
DataSourceContextHolder.setDataSourceType(dataSourceType);
// 執(zhí)行數(shù)據(jù)庫操作
}
3. 基于注解的切換策略
基于注解的策略允許在方法或類上直接使用注解來指定數(shù)據(jù)源,從而簡化代碼邏輯。這種方式可以更加靈活地在不同的服務(wù)方法中指定數(shù)據(jù)源,通常是使用自定義注解來標(biāo)記需要使用的具體數(shù)據(jù)源。創(chuàng)建一個自定義注解,例如 @TargetDataSource,在方法級別使用該注解來切換數(shù)據(jù)源。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String value();
}
使用該注解來標(biāo)記需要切換數(shù)據(jù)源的方法:
@Service
public class SomeService {
@TargetDataSource("master")
public void writeData() {
// 執(zhí)行寫操作
}
@TargetDataSource("slave")
public void readData() {
// 執(zhí)行讀操作
}
}
創(chuàng)建一個切面(Aspect)來攔截帶有 @TargetDataSource 注解的方法,并動態(tài)切換數(shù)據(jù)源:
@Aspect
@Component
public class DataSourceAspect {
@Around("@annotation(targetDataSource)")
public Object switchDataSource(ProceedingJoinPoint point, TargetDataSource targetDataSource) throws Throwable {
// 切換數(shù)據(jù)源
DataSourceContextHolder.setDataSourceType(targetDataSource.value());
try {
return point.proceed();
} finally {
// 切換回默認(rèn)數(shù)據(jù)源
DataSourceContextHolder.clearDataSourceType();
}
}
}
4. 基于請求類型的切換策略
在某些場景下,切換策略可以基于請求類型進(jìn)行判斷,例如區(qū)分 API 接口、用戶身份或請求的 URI 等。通過攔截請求的 URL 或參數(shù)來確定使用哪個數(shù)據(jù)源??梢允褂?Spring 的 HandlerInterceptor 或自定義的過濾器來攔截請求并根據(jù)請求類型進(jìn)行數(shù)據(jù)源切換。
@Component
public class RequestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String path = request.getRequestURI();
if (path.contains("/admin")) {
// 切換到主庫
DataSourceContextHolder.setDataSourceType("master");
} else {
// 切換到從庫
DataSourceContextHolder.setDataSourceType("slave");
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理數(shù)據(jù)源
DataSourceContextHolder.clearDataSourceType();
}
}
5. 基于事務(wù)的切換策略
在某些復(fù)雜場景下,可能需要通過事務(wù)來控制數(shù)據(jù)源的切換,確保同一個事務(wù)中的多個操作使用相同的數(shù)據(jù)源。Spring 提供的 @Transactional 注解可以幫助實(shí)現(xiàn)事務(wù)管理,在事務(wù)中進(jìn)行數(shù)據(jù)源的切換。在方法上使用 @Transactional 注解進(jìn)行事務(wù)管理,并在方法內(nèi)切換數(shù)據(jù)源。
@Transactional
public void processTransaction() {
DataSourceContextHolder.setDataSourceType("master");
// 執(zhí)行事務(wù)相關(guān)操作
}
6. 基于業(yè)務(wù)類型的切換策略
有時候業(yè)務(wù)需求決定了數(shù)據(jù)源的選擇。例如,某些業(yè)務(wù)模塊專門使用主庫,另一些業(yè)務(wù)模塊則專門使用從庫。通過業(yè)務(wù)模塊類型來決定數(shù)據(jù)源切換。根據(jù)業(yè)務(wù)模塊的類型來動態(tài)切換數(shù)據(jù)源,常見的是通過方法參數(shù)或業(yè)務(wù)配置來決定。
public void processBusinessRequest(String businessType) {
if ("financial".equals(businessType)) {
DataSourceContextHolder.setDataSourceType("master");
} else {
DataSourceContextHolder.setDataSourceType("slave");
}
// 執(zhí)行業(yè)務(wù)操作
}
AOP實(shí)現(xiàn)數(shù)據(jù)源切換
使用 AOP(面向切面編程)來實(shí)現(xiàn)數(shù)據(jù)源切換是一種非常常見和優(yōu)雅的做法。AOP 允許我們在不修改業(yè)務(wù)代碼的情況下,動態(tài)地切換數(shù)據(jù)源。通過 AOP,我們可以在方法執(zhí)行前或執(zhí)行后切換數(shù)據(jù)源,依據(jù)方法的特性(如讀寫操作或注解)來決定使用哪個數(shù)據(jù)源。
1. 概述
AOP 通過切面(Aspect)來攔截方法的調(diào)用,在方法執(zhí)行前后進(jìn)行一些操作。在實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換時,通常會基于以下幾個要素進(jìn)行切換:
- ?業(yè)務(wù)邏輯類型(如讀或?qū)懀?/li>
- ?自定義注解
- ?方法執(zhí)行的上下文(如請求、用戶權(quán)限等)
2. 使用 AOP 實(shí)現(xiàn)數(shù)據(jù)源切換的基本步驟
- 定義數(shù)據(jù)源上下文:使用 ThreadLocal 或類似方式在當(dāng)前線程中保存當(dāng)前的數(shù)據(jù)源。
- 創(chuàng)建自定義注解:用來標(biāo)記需要切換數(shù)據(jù)源的方法或類。
- 編寫切面:使用 AOP 攔截器來攔截指定方法或類,并根據(jù)自定義注解來動態(tài)切換數(shù)據(jù)源。
- 切換數(shù)據(jù)源:在切面中根據(jù)業(yè)務(wù)邏輯切換數(shù)據(jù)源。
- 清理數(shù)據(jù)源:方法執(zhí)行后清理當(dāng)前數(shù)據(jù)源的上下文,避免影響到其他方法。
3. 實(shí)現(xiàn)步驟
1.定義數(shù)據(jù)源上下文
首先,我們需要定義一個 DataSourceContextHolder 類來存儲當(dāng)前的數(shù)據(jù)源類型。可以使用 ThreadLocal 來確保每個線程都有獨(dú)立的數(shù)據(jù)源。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 設(shè)置當(dāng)前線程的數(shù)據(jù)源
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
// 獲取當(dāng)前線程的數(shù)據(jù)源
public static String getDataSourceType() {
return contextHolder.get();
}
// 清除當(dāng)前線程的數(shù)據(jù)源
public static void clearDataSourceType() {
contextHolder.remove();
}
}
2.定義自定義注解
我們定義一個注解 @TargetDataSource,用于標(biāo)記需要切換數(shù)據(jù)源的方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String value(); // 數(shù)據(jù)源名稱
}
3.編寫切面
使用 AOP 來創(chuàng)建一個切面類,在方法執(zhí)行前根據(jù)注解中的值來切換數(shù)據(jù)源。切面會攔截所有使用 @TargetDataSource 注解的方法,并在執(zhí)行時切換數(shù)據(jù)源。
@Aspect
@Component
public class DataSourceAspect {
// 方法執(zhí)行前切換數(shù)據(jù)源
@Before("@annotation(targetDataSource)")
public void before(JoinPoint point, TargetDataSource targetDataSource) {
String dataSource = targetDataSource.value();
DataSourceContextHolder.setDataSourceType(dataSource); // 切換數(shù)據(jù)源
}
// 方法執(zhí)行后清理數(shù)據(jù)源
@After("@annotation(targetDataSource)")
public void after(JoinPoint point, TargetDataSource targetDataSource) {
DataSourceContextHolder.clearDataSourceType(); // 清除數(shù)據(jù)源
}
}
4.使用注解切換數(shù)據(jù)源
在業(yè)務(wù)邏輯中,使用 @TargetDataSource 注解標(biāo)記需要切換數(shù)據(jù)源的方法。根據(jù)注解的值,切面會決定使用哪個數(shù)據(jù)源。
@Service
public class MyService {
@TargetDataSource("master")
public void writeData() {
// 執(zhí)行寫操作,使用主數(shù)據(jù)庫
}
@TargetDataSource("slave")
public void readData() {
// 執(zhí)行讀操作,使用從數(shù)據(jù)庫
}
}
5) 配置數(shù)據(jù)源
在 Spring Boot 配置文件中,配置多個數(shù)據(jù)源,并確保數(shù)據(jù)源切換的邏輯正常工作。
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/masterdb
username: root
password: password
slave:
url: jdbc:mysql://localhost:3306/slavedb
username: root
password: password異常與回滾處理
? 在使用動態(tài)數(shù)據(jù)源切換時,異常和回滾處理是非常重要的,因?yàn)樵跀?shù)據(jù)源切換的過程中可能會遇到各種異常,且如果沒有適當(dāng)?shù)幕貪L機(jī)制,可能會導(dǎo)致數(shù)據(jù)不一致或其他嚴(yán)重問題。為了確保系統(tǒng)的可靠性和一致性,我們需要在處理數(shù)據(jù)源切換時合理地管理事務(wù)和異常。
1. 問題背景
? 在分布式或多數(shù)據(jù)源的應(yīng)用場景中,通常需要根據(jù)不同的業(yè)務(wù)需求切換數(shù)據(jù)源。例如,在讀寫分離的場景中,讀操作使用從數(shù)據(jù)庫,而寫操作使用主數(shù)據(jù)庫。動態(tài)切換數(shù)據(jù)源時,涉及多個數(shù)據(jù)源的連接和事務(wù)管理,這使得異常處理和回滾更加復(fù)雜。如果在執(zhí)行操作時遇到異常,沒有適當(dāng)?shù)幕貪L處理,可能導(dǎo)致數(shù)據(jù)庫中的數(shù)據(jù)不一致,甚至事務(wù)未能正確完成。
2. 異常處理的重要性
動態(tài)數(shù)據(jù)源切換和數(shù)據(jù)庫操作中可能會發(fā)生不同類型的異常,這些異常可能來自于:
- ?數(shù)據(jù)源連接失敗
- ?數(shù)據(jù)庫查詢或更新失敗
- ?事務(wù)提交失敗
- ?跨多個數(shù)據(jù)源的事務(wù)不一致問題
因此,我們必須采取合理的措施來捕獲異常并回滾事務(wù),保證系統(tǒng)的穩(wěn)定性和一致性。
3. 異常和回滾處理的關(guān)鍵步驟
1) 事務(wù)管理
對于多數(shù)據(jù)源的應(yīng)用,事務(wù)管理至關(guān)重要。在 Spring 中,我們可以通過 @Transactional 注解來管理事務(wù),它能夠確保操作的原子性和一致性。事務(wù)注解可以應(yīng)用于服務(wù)層的方法,自動啟動和提交事務(wù)。
@Service
public class MyService {
@Transactional
public void handleTransaction() {
try {
// 切換到主數(shù)據(jù)源進(jìn)行寫操作
dataSourceService.writeData();
// 切換到從數(shù)據(jù)源進(jìn)行讀操作
dataSourceService.readData();
} catch (Exception e) {
// 異常處理和回滾
handleException(e);
}
}
private void handleException(Exception e) {
// 記錄異常信息
System.out.println("Exception occurred: " + e.getMessage());
// 手動回滾事務(wù)(Spring會自動回滾,但可以根據(jù)需要自定義)
throw new RuntimeException("Transaction failed, performing rollback.");
}
}
在 @Transactional 注解下,Spring 會自動為所有參與的操作開啟事務(wù),如果有異常拋出,Spring 會自動回滾事務(wù)。
2) 動態(tài)數(shù)據(jù)源切換時的回滾機(jī)制
當(dāng)我們進(jìn)行數(shù)據(jù)源切換時,必須保證在出現(xiàn)異常時能夠正確地進(jìn)行回滾。為了實(shí)現(xiàn)這一點(diǎn),可以通過以下幾種方式:
- ?全局回滾:在多數(shù)據(jù)源環(huán)境中,使用一個全局事務(wù)管理器(如 Atomikos、Narayana 等)來管理多個數(shù)據(jù)源的事務(wù)。這樣可以保證在一個數(shù)據(jù)源操作失敗時,所有相關(guān)數(shù)據(jù)源的操作都會回滾,保證數(shù)據(jù)一致性。
- ?本地回滾:如果只是切換了一個數(shù)據(jù)源,且沒有涉及多個數(shù)據(jù)源的操作,Spring 的
@Transactional注解可以確保數(shù)據(jù)源相關(guān)的事務(wù)回滾。
3) 捕獲與處理異常
在多數(shù)據(jù)源和多事務(wù)的環(huán)境下,異常處理尤為重要。以下是一些常見的異常處理策略:
- ?捕獲并記錄異常:及時捕獲異常并記錄日志,以便追蹤和調(diào)試問題。
- ?事務(wù)回滾:在數(shù)據(jù)源切換時,如果發(fā)生異常,必須確保事務(wù)能夠正確回滾,避免造成數(shù)據(jù)不一致。
- ?根據(jù)異常類型判斷回滾:使用 @Transactional(rollbackFor = Exception.class) 來控制特定異常時是否回滾事務(wù)。例如,可以根據(jù)業(yè)務(wù)需求,選擇只針對某些特定異常進(jìn)行回滾。
@Transactional(rollbackFor = SQLException.class)
public void someMethod() throws SQLException {
// 數(shù)據(jù)庫操作
}
4) 數(shù)據(jù)源恢復(fù)與錯誤重試機(jī)制
在動態(tài)數(shù)據(jù)源切換時,數(shù)據(jù)源的不可用或錯誤可能導(dǎo)致操作失敗。為了提升系統(tǒng)的健壯性,可以加入數(shù)據(jù)源恢復(fù)或重試機(jī)制:
- ?重試機(jī)制:在出現(xiàn)臨時性異常時,可以嘗試重新連接數(shù)據(jù)庫。Spring 提供了 @Retryable 注解,可以幫助實(shí)現(xiàn)方法級別的重試邏輯。
- ?數(shù)據(jù)源切換失敗后的恢復(fù)機(jī)制:在數(shù)據(jù)源連接失敗時,可以切換到備用數(shù)據(jù)源或進(jìn)行自動恢復(fù)操作。
5) 跨數(shù)據(jù)庫事務(wù)管理(分布式事務(wù))
如果涉及多個數(shù)據(jù)源操作,并且需要保證跨多個數(shù)據(jù)庫的事務(wù)一致性,可以使用分布式事務(wù)框架來管理。常用的分布式事務(wù)框架有:
- ?Atomikos:一個開源的 Java 分布式事務(wù)管理器,支持多數(shù)據(jù)源的事務(wù)管理。
- ?Seata:一個現(xiàn)代化的分布式事務(wù)框架,支持高效地管理分布式事務(wù)。
通過這些框架,我們可以保證在多個數(shù)據(jù)庫間操作時的一致性和可靠性。
4. 示例:使用 Atomikos 進(jìn)行分布式事務(wù)管理
@Service
public class MyService {
@Transactional
public void handleTransaction() throws SQLException {
try {
// 執(zhí)行主數(shù)據(jù)庫操作
dataSourceService.writeData();
// 執(zhí)行從數(shù)據(jù)庫操作
dataSourceService.readData();
} catch (Exception e) {
// 在分布式事務(wù)框架下,異常發(fā)生時自動回滾所有操作
System.out.println("Exception occurred, performing rollback: " + e.getMessage());
throw new RuntimeException("Transaction failed, performing rollback.");
}
}
}
通過 Atomikos 等分布式事務(wù)框架,事務(wù)將在多個數(shù)據(jù)源之間自動進(jìn)行協(xié)調(diào),并保證跨數(shù)據(jù)源操作的一致性。
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
- SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源連接和切換的完整方案
- 一文詳解SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的自動切換
- SpringBoot和MyBatis環(huán)境下實(shí)現(xiàn)動態(tài)數(shù)據(jù)源切換過程
- SpringBoot實(shí)現(xiàn)動態(tài)切換數(shù)據(jù)源的示例代碼
- 在SpringBoot項(xiàng)目中動態(tài)切換數(shù)據(jù)源和數(shù)據(jù)庫的詳細(xì)步驟
- SpringBoot實(shí)現(xiàn)Redis多數(shù)據(jù)庫切換(多數(shù)據(jù)源配置)
相關(guān)文章
Java實(shí)現(xiàn)XML與JSON的互相轉(zhuǎn)換詳解
這篇文章主要為大家詳細(xì)介紹了如何使用Java實(shí)現(xiàn)XML與JSON的互相轉(zhuǎn)換,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03
SpringCloud Alibaba Sentinel 從入門到精通
本文介紹了Sentinel的核心概念、使用方式及規(guī)則配置,Sentinel是阿里巴巴開源的流量控制框架,主要應(yīng)用于流量控制、熔斷降級等場景,文中有詳細(xì)的規(guī)則配置示例,并介紹了如何與Feign整合,感興趣的朋友一起看看吧2026-04-04
Spring?Boot?項(xiàng)目中?JPA?語法的基本使用方法
這篇文章主要介紹了?Spring?Boot?項(xiàng)目中?JPA?語法的基本使用方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-10-10
基于Java實(shí)現(xiàn)ssh命令登錄主機(jī)執(zhí)行shell命令過程解析
這篇文章主要介紹了基于Java實(shí)現(xiàn)ssh命令登錄主機(jī)執(zhí)行shell命令過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12
解決nacos項(xiàng)目啟動報錯:Connection refused: no further&
這篇文章主要介紹了解決nacos項(xiàng)目啟動報錯:Connection refused: no further informa問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04
springboot啟動后和停止前執(zhí)行方法示例詳解
這篇文章主要介紹了springboot啟動后和停止前執(zhí)行方法,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08
ReentrantReadWriteLock不能鎖升級的原因總結(jié)
今天給大家?guī)淼氖顷P(guān)于Java并發(fā)的相關(guān)知識,文章圍繞著為什么ReentrantReadWriteLock不能鎖升級展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06

