1. 動態切換數據源的原理
AbstractRoutingDataSource 是 Spring 提供的一個抽象類,它通過實現 determineCurrentLookupKey 方法,根據上下文信息決定當前使用的數據源。核心流程如下:
- 定義多數據源配置:注冊多個數據源。
- 實現動態數據源路由:繼承 AbstractRoutingDataSource,根據上下文返回數據源標識。
- 使用攔截器設置上下文:在請求中設置當前使用的數據源。
?2. 實現步驟
2.1 確保你的 pom.xml 中已經包含如下依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>
?
2.2 繼承自 Spring 提供的抽象類 AbstractRoutingDataSource
package com.imooc.cloud.springboot;import com.imooc.cloud.dynamic.raw.DataSourceContext;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import java.util.Map;public class SpringDynamicDataSource extends AbstractRoutingDataSource {public SpringDynamicDataSource(Map<Object, Object> targetDataSources) {super.setTargetDataSources(targetDataSources);}@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContext.getCurrentDb();}
}
類定義
public class SpringDynamicDataSource extends AbstractRoutingDataSource {
}
- 繼承自 Spring 提供的抽象類
AbstractRoutingDataSource
。 - 是實現多數據源切換的核心類。
構造函數
public SpringDynamicDataSource(Map<Object, Object> targetDataSources) {super.setTargetDataSources(targetDataSources);
}
- 通過構造器傳入多個目標數據源(通常是
Map<標識符, DataSource>
形式)。 - 調用父類方法設置這些數據源。
核心方法:determineCurrentLookupKey()
@Override
protected Object determineCurrentLookupKey() {return DataSourceContext.getCurrentDb();
}
- Spring 框架會在每次數據庫操作時調用這個方法。
- 返回當前線程使用的數據源標識(如
"master"
、"slave1"
)。 - 實際上是從
ThreadLocal
中獲取當前線程綁定的數據源名稱。
2.3 數據源上下文工具類?
public class DataSourceContext {private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();public static void setCurrentDb(String db) {CONTEXT.set(db);}public static String getCurrentDb() {return CONTEXT.get();}public static void removeCurrentDb() {CONTEXT.remove();}
}
用于保存和清除當前線程使用的數據源標識。
2.4?將多數據源注入并創建 SpringDynamicDataSource
package com.imooc.cloud.springboot;import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;public class SpringDataSourceConfiguration {@Beanpublic DataSource mybatisPlusDataSource() {return DataSourceBuilder.create().driverClassName("com.mysql.jdbc.Driver").url("jdbc:mysql://192.168.3.150:3306/mybatisplus?characterEncoding=utf8").username("root").password("123456").build();}@Beanpublic DataSource mybatisExampleDataSource() {return DataSourceBuilder.create().driverClassName("com.mysql.jdbc.Driver").url("jdbc:mysql://192.168.3.150:3306/mybatis-example?characterEncoding=utf8").username("root").password("123456").build();}@Primary@Beanpublic SpringDynamicDataSource springDynamicDataSource() {Map<Object, Object> targetDataSources = new HashMap<>();DataSource mybatisPlusDataSource = mybatisPlusDataSource();DataSource mybatisExampleDataSource = mybatisExampleDataSource();targetDataSources.put("mybatisPlus", mybatisPlusDataSource);targetDataSources.put("mybatisExample", mybatisExampleDataSource);return new SpringDynamicDataSource(targetDataSources);}
}
2.5?安全地保存和切換當前線程使用的數據源
在多線程環境下,安全地保存和切換當前線程使用的數據源標識(如
"master"
、"slave1"
等),支持嵌套調用(例如在事務中嵌套切換數據源),并且使用 雙端隊列(Deque)模擬棧結構 來管理數據源切換的上下文。?
- 使用
ThreadLocal
保存每個線程獨立的 數據源棧(Deque)。 - 使用
NamedThreadLocal
有助于在調試或日志中識別該線程局部變量的用途。 ArrayDeque
是一個雙端隊列,這里用作棧(LIFO),實現嵌套切換數據源的功能。
package com.imooc.cloud.util;import org.springframework.core.NamedThreadLocal;
import org.springframework.util.StringUtils;import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Objects;public final class DynamicDataSourceContextHolder {/*** 雙端隊列其實本質就是一個棧*/private static final ThreadLocal<Deque<String>> DATASOURCE_CONTEXT = NamedThreadLocal.withInitial(() -> new ArrayDeque<>());private DynamicDataSourceContextHolder() {if (DATASOURCE_CONTEXT != null) {throw new RuntimeException("禁止反射創建");}}public static String getCurrentDataSource() {//todo 2023-07-31 修復補丁。因為可能返回null,而ConcurrentHashMap的get方法不能傳入null,否則報空指針String peek = DATASOURCE_CONTEXT.get().peek();return Objects.isNull(peek) ? "" : peek;}public static String addDataSource(String dds) {String datasource = StringUtils.isEmpty(dds) ? "" : dds;DATASOURCE_CONTEXT.get().push(datasource);return datasource;}public static void removeCurrentDataSource() {Deque<String> deque = DATASOURCE_CONTEXT.get();deque.poll();if (deque.isEmpty()) {DATASOURCE_CONTEXT.remove();}}
}
單例構造限制
private DynamicDataSourceContextHolder() {if (DATASOURCE_CONTEXT != null) {throw new RuntimeException("禁止反射創建");}
}
- 私有構造方法,防止外部實例化。
- 添加了反射創建檢測,防止通過反射破壞單例。
獲取當前數據源
public static String getCurrentDataSource() {String peek = DATASOURCE_CONTEXT.get().peek();return Objects.isNull(peek) ? "" : peek;
}
- 從當前線程的數據源棧中獲取當前使用的數據源標識。
- 如果棧為空,返回空字符串
""
,避免后續操作(如 Map.get(null))導致空指針異常。
設置新數據源(入棧)
public static String addDataSource(String dds) {String datasource = StringUtils.isEmpty(dds) ? "" : dds;DATASOURCE_CONTEXT.get().push(datasource);return datasource;
}
- 將指定的數據源標識壓入棧頂。
- 支持嵌套切換數據源(例如 AOP + 事務中嵌套注解切換)。
- 如果傳入
null
或空字符串,則使用默認空字符串。
?移除當前數據源(出棧)
public static void removeCurrentDataSource() {Deque<String> deque = DATASOURCE_CONTEXT.get();deque.poll();if (deque.isEmpty()) {DATASOURCE_CONTEXT.remove();}
}
- 從棧中彈出一個數據源標識(LIFO)。
- 如果棧為空,則清除整個線程局部變量,防止內存泄漏。
這個工具類通常用于配合 動態數據源路由類(如 AbstractRoutingDataSource
)一起使用,實現多數據源切換。例如:
1. 動態數據源路由類(簡化版)?
public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getCurrentDataSource();}
}
2. AOP 切面控制數據源切換
@Aspect
@Component
public class DataSourceAspect {@Before("@annotation(ds))")public void beforeSwitchDS(JoinPoint point, DynamicDataSource ds) {DynamicDataSourceContextHolder.addDataSource(ds.db());}@After("@annotation(ds))")public void afterSwitchDS(JoinPoint point, DynamicDataSource ds) {DynamicDataSourceContextHolder.removeCurrentDataSource();}
}
3. 注解定義
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {String db() default "master";
}
4. Service 使用示例
@Service
public class UserService {@DynamicDataSource("slave1")public List<User> queryFromSlave() {return userMapper.selectAll();}public void insertUser(User user) {userMapper.insert(user);}
}
3. 測試
package com.imooc.cloud;import com.imooc.cloud.util.DynamicDataSourceContextHolder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;import java.util.List;@SpringBootTest
public class SpringDynamicTest {@Autowiredprivate JdbcTemplate jdbcTemplate;@Testpublic void testQueryUser() {DynamicDataSourceContextHolder.addDataSource("mybatisPlus");List list = jdbcTemplate.queryForList("select * from user");System.out.println("list: "+list);}@Testpublic void testQueryOrder() {DynamicDataSourceContextHolder.addDataSource("mybatisExample");List list = jdbcTemplate.queryForList("select * from `user`");System.out.println("list: "+list);}
}
4.?完整使用流程圖
+-----------------+
| @DynamicDataSource("slave1") |
+-----------------+↓
+----------------------+
| AOP Before Advice |
| DynamicDataSourceContextHolder.addDataSource("slave1") |
+----------------------+↓
+----------------------+
| AbstractRoutingDataSource.determineCurrentLookupKey() |
| return DynamicDataSourceContextHolder.getCurrentDataSource() |
+----------------------+↓
+----------------------+
| JDBC / MyBatis 使用對應數據源執行 SQL |
+----------------------+↓
+----------------------+
| AOP After Advice |
| DynamicDataSourceContextHolder.removeCurrentDataSource() |
+----------------------+
5.?推薦使用 dynamic-datasource-spring-boot-starter
新項目,強烈建議使用開源組件來簡化多數據源配置:
1. 引入依賴
<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>4.2.0</version>
</dependency>
2. 配置文件(application.yml)
spring:datasource:dynamic:primary: masterdatasource:master:url: jdbc:mysql://localhost:3306/masterusername: rootpassword: rootslave1:url: jdbc:mysql://localhost:3306/slave1username: rootpassword: root
3. 使用注解
@DS("slave1")
public List<User> queryFromSlave() {return userMapper.selectList(null);
}
🎯 總結
功能 | 說明 |
---|---|
DynamicDataSourceContextHolder | 數據源上下文管理工具 |
Deque<String> | 支持嵌套切換 |
ThreadLocal | 線程隔離 |
AOP + 注解 | 實現優雅的數據源切換 |
dynamic-datasource-spring-boot-starter | 推薦使用的封裝庫 |