# SpringBoot + DynamicDataSource 實現動態添加切換數據源
## 一、前言
在現代企業級應用開發中,多數據源的需求變得越來越普遍。無論是出于分庫分表的考慮,還是需要連接不同業務系統的數據庫,動態數據源切換都成為了必備技能。本文將詳細介紹如何在SpringBoot項目中通過dynamic-datasource框架實現動態添加和切換數據源。
## 二、動態數據源核心概念
### 2.1 什么是動態數據源
動態數據源(Dynamic DataSource)是指應用程序在運行時能夠根據需要切換不同的數據庫連接。與傳統的單一數據源不同,動態數據源具有以下特點:
1. 運行時動態添加新數據源
2. 支持多數據源之間的自由切換
3. 可根據業務邏輯自動選擇數據源
### 2.2 常見應用場景
- 多租戶SaaS系統
- 讀寫分離架構
- 分庫分表實現
- 異構數據庫集成
## 三、技術選型
### 3.1 主流實現方案對比
| 方案 | 優點 | 缺點 |
|---------------------|-----------------------------|-----------------------------|
| AbstractRoutingDataSource | Spring原生支持,無需額外依賴 | 功能較為基礎,缺少高級特性 |
| dynamic-datasource | 功能豐富,文檔完善 | 需要引入第三方依賴 |
| MyBatis多數據源 | 與MyBatis深度集成 | 對其他ORM框架支持不足 |
### 3.2 為什么選擇dynamic-datasource
1. 支持數據源分組(主從架構)
2. 提供豐富的SPI擴展點
3. 內置敏感信息加密
4. 支持Seata分布式事務
5. 活躍的社區維護
## 四、環境準備
### 4.1 項目依賴
```xml
<dependencies>
<!-- SpringBoot基礎依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 數據庫相關 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- 其他工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
spring:
datasource:
dynamic:
primary: master # 設置默認數據源
datasource:
master:
url: jdbc:mysql://localhost:3306/master_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1:
url: jdbc:mysql://localhost:3306/slave_db_1
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
創建數據源注冊工具類:
@Slf4j
@Component
public class DataSourceRegisterUtil {
@Autowired
private DataSourcePropertiesCreator propertiesCreator;
@Autowired
private DynamicRoutingDataSource routingDataSource;
/**
* 注冊新數據源
* @param poolName 數據源名稱
* @param driverClassName 驅動類
* @param url 數據庫URL
* @param username 用戶名
* @param password 密碼
*/
public synchronized void register(String poolName,
String driverClassName,
String url,
String username,
String password) {
// 檢查是否已存在
if (routingDataSource.getDataSources().containsKey(poolName)) {
log.warn("數據源[{}]已存在,將被覆蓋", poolName);
}
// 創建數據源配置
DataSourceProperty property = new DataSourceProperty();
property.setPoolName(poolName);
property.setDriverClassName(driverClassName);
property.setUrl(url);
property.setUsername(username);
property.setPassword(password);
// 創建數據源
DataSource dataSource = propertiesCreator.createDataSource(property);
// 注冊到動態數據源
routingDataSource.addDataSource(poolName, dataSource);
log.info("數據源[{}]注冊成功", poolName);
}
}
創建數據源注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
String value() default "";
}
創建切面處理:
@Aspect
@Component
@Order(-1) // 確保在事務注解前執行
@Slf4j
public class DynamicDataSourceAspect {
@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint point, DS ds) throws Throwable {
String dsKey = ds.value();
if (!DynamicDataSourceContextHolder.containsDataSource(dsKey)) {
log.error("數據源[{}]不存在,使用默認數據源", dsKey);
} else {
DynamicDataSourceContextHolder.push(dsKey);
log.debug("切換數據源到: {}", dsKey);
}
try {
return point.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
log.debug("恢復數據源到: {}",
DynamicDataSourceContextHolder.peek());
}
}
}
public class DataSourceContextHolder {
public static void setDataSource(String dsName) {
if (!DynamicDataSourceContextHolder.containsDataSource(dsName)) {
throw new IllegalArgumentException("數據源"+dsName+"不存在");
}
DynamicDataSourceContextHolder.push(dsName);
}
public static void clear() {
DynamicDataSourceContextHolder.poll();
}
public static String getCurrentDataSource() {
return DynamicDataSourceContextHolder.peek();
}
}
創建REST接口:
@RestController
@RequestMapping("/api/datasource")
public class DataSourceController {
@Autowired
private DataSourceRegisterUtil registerUtil;
@PostMapping("/add")
public Result addDataSource(@RequestBody DataSourceDTO dto) {
try {
registerUtil.register(
dto.getPoolName(),
dto.getDriverClassName(),
dto.getUrl(),
dto.getUsername(),
dto.getPassword()
);
return Result.success();
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
}
@Data
class DataSourceDTO {
private String poolName;
private String driverClassName;
private String url;
private String username;
private String password;
}
@Component
public class DataSourceHealthChecker {
@Autowired
private DynamicRoutingDataSource routingDataSource;
private final Map<String, Boolean> healthStatus = new ConcurrentHashMap<>();
@Scheduled(fixedDelay = 30000)
public void checkAllDataSources() {
Map<String, DataSource> dataSources = routingDataSource.getDataSources();
dataSources.forEach((name, ds) -> {
boolean healthy = testConnection(ds);
healthStatus.put(name, healthy);
if (!healthy) {
log.error("數據源[{}]連接異常", name);
}
});
}
private boolean testConnection(DataSource dataSource) {
try (Connection conn = dataSource.getConnection()) {
return conn.isValid(3);
} catch (SQLException e) {
return false;
}
}
public boolean isHealthy(String dsName) {
return healthStatus.getOrDefault(dsName, false);
}
}
實現讀數據源的輪詢負載均衡:
@Component
public class ReadDataSourceLoadBalancer {
@Autowired
private DynamicRoutingDataSource routingDataSource;
private final AtomicInteger counter = new AtomicInteger(0);
public String selectReadDataSource() {
List<String> readDataSources = routingDataSource.getDataSources()
.keySet().stream()
.filter(name -> name.startsWith("slave_"))
.collect(Collectors.toList());
if (readDataSources.isEmpty()) {
return null;
}
int index = counter.getAndIncrement() % readDataSources.size();
if (counter.get() > 10000) {
counter.set(0);
}
return readDataSources.get(index);
}
}
在多數據源環境下,事務處理需要特別注意:
@DS
注解必須放在@Transactional
之前REQUIRES_NEW
spring:
datasource:
dynamic:
datasource:
master:
hikari:
maximum-pool-size: 20
minimum-idle: 5
現象:注解切換不生效,始終使用默認數據源
排查步驟:
1. 檢查@DS
注解是否被正確掃描
2. 確認切面執行順序高于事務切面
3. 檢查數據源名稱是否拼寫正確
可能原因: 1. 新數據源配置有誤 2. 未正確刷新數據源集合
解決方案:
// 添加數據源后手動刷新
routingDataSource.getDataSources().put(poolName, dataSource);
routingDataSource.afterPropertiesSet();
本文詳細介紹了在SpringBoot項目中實現動態數據源的全過程,包括:
通過本文的指導,開發者可以快速在項目中實現靈活的多數據源管理,滿足復雜業務場景下的數據訪問需求。
附錄:完整配置示例
spring:
datasource:
dynamic:
primary: master
strict: true # 嚴格模式匹配數據源
datasource:
master:
url: jdbc:mysql://localhost:3306/master_db?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
connection-timeout: 30000
maximum-pool-size: 20
slave_1:
url: jdbc:mysql://localhost:3306/slave_1?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid: # 公共druid配置
initial-size: 5
max-active: 20
min-idle: 5
”`
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。