markdown
# Spring Boot 多数据源手动切换实现指南
在实际项目开发中,经常会遇到需要连接多个数据库的场景,例如:
- 读写分离(主库写,从库读)
- 不同业务模块使用不同数据库
- 数据迁移、报表系统独立数据库
本文将详细介绍如何在 Spring Boot 项目中实现 **多数据源的手动切换**,并通过 `DynamicDataSource` + `ThreadLocal` 的方式灵活控制数据源路由。
---
## 🎯 一、核心目标
- 支持配置多个数据源(如 `master`、`slave`)
- 在运行时 **手动指定** 使用哪个数据源
- 切换过程对业务透明,不影响原有 JDBC/MyBatis 调用方式
- 线程安全,避免数据源错乱
---
## 🔧 二、技术选型
| 技术 | 说明 |
|------|------|
| `AbstractRoutingDataSource` | Spring 提供的抽象类,支持动态路由数据源 |
| `ThreadLocal` | 保证每个线程持有独立的数据源标识 |
| `DataSourceContextHolder` | 自定义上下文工具类 |
| `DynamicDataSource` | 继承 `AbstractRoutingDataSource`,实现动态路由逻辑 |
---
## 📦 三、项目结构
src/ ├── main/ │ ├── java/ │ │ └── com/example/datasource/ │ │ ├── config/ # 配置类 │ │ │ ├── DataSourceConfig.java │ │ │ └── DynamicDataSource.java │ │ ├── context/ # 上下文工具 │ │ │ └── DataSourceContextHolder.java │ │ ├── service/ │ │ │ └── UserService.java # 业务示例 │ │ └── Application.java │ └── resources/ │ └── application.yml
yaml
---
## 🛠️ 四、详细实现步骤
### 1. 配置文件 `application.yml`
```yaml
spring:
datasource:
master:
url: jdbc:mysql://localhost:3306/db_master?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/db_slave?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
2. 数据源上下文持有者(DataSourceContextHolder
)
使用 ThreadLocal
存储当前线程的数据源标识。
java
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* 设置当前线程使用的数据源
*/
public static void setDataSource(String dataSourceKey) {
contextHolder.set(dataSourceKey);
}
/**
* 获取当前线程的数据源
*/
public static String getDataSource() {
return contextHolder.get();
}
/**
* 清除当前数据源(防止线程复用导致污染)
*/
public static void clear() {
contextHolder.remove();
}
}
⚠️ 必须在
finally
块中调用clear()
,避免内存泄漏和线程污染。
3. 动态数据源类(DynamicDataSource
)
继承 AbstractRoutingDataSource
,重写 determineCurrentLookupKey()
方法。
java
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
✅
determineCurrentLookupKey()
是 Spring 在每次获取连接时自动调用的方法,返回值用于查找目标数据源。
4. 数据源配置类(DataSourceConfig
)
注册多个数据源,并将 DynamicDataSource
作为主数据源。
java
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); // 默认数据源
return dynamicDataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
5. 业务层手动切换示例
java
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Map<String, Object>> getUsersFromMaster() {
DataSourceContextHolder.setDataSource("master");
try {
return jdbcTemplate.queryForList("SELECT * FROM users");
} finally {
DataSourceContextHolder.clear(); // 必须清理
}
}
public List<Map<String, Object>> getUsersFromSlave() {
DataSourceContextHolder.setDataSource("slave");
try {
return jdbcTemplate.queryForList("SELECT * FROM users");
} finally {
DataSourceContextHolder.clear(); // 必须清理
}
}
}
🔍 五、核心原理剖析
1. 联动机制流程
scss
[业务代码]
↓
DataSourceContextHolder.setDataSource("slave")
↓
JdbcTemplate.query(...) → 获取连接
↓
AbstractRoutingDataSource.getConnection()
↓
determineTargetDataSource()
↓
lookupKey = determineCurrentLookupKey() → 返回 "slave"
↓
dataSource = resolvedDataSources.get(lookupKey) → 从 Map 中查找
↓
return dataSource.getConnection()
2. 关键源码解析(AbstractRoutingDataSource
)
java
protected DataSource determineTargetDataSource() {
Object lookupKey = determineCurrentLookupKey(); // 调用我们重写的方法
DataSource dataSource = this.resolvedDataSources.get(lookupKey); // 根据 key 查找
if (dataSource == null && this.lenientFallback) {
dataSource = this.resolvedDefaultDataSource;
}
return dataSource;
}
✅ "根据 key 查找数据源"的逻辑由 Spring 框架自动实现,开发者无需手动编码。
⚠️ 六、注意事项
-
必须清理 ThreadLocal
每次使用完必须调用
DataSourceContextHolder.clear()
,防止线程池中线程复用导致数据源错乱。 -
事务管理器需指向
DynamicDataSource
如果使用
@Transactional
,确保PlatformTransactionManager
使用的是动态数据源。 -
不建议用于分布式事务
多数据源涉及跨库事务时,应使用 Seata、XA 等分布式事务方案。
-
避免在异步线程中切换失效
ThreadLocal
不会自动传递到子线程,异步场景需手动传递或使用InheritableThreadLocal
。
🧩 七、适用场景
场景 | 说明 |
---|---|
读写分离 | 主库写,从库读,提升性能 |
多租户系统 | 不同租户使用不同数据库 |
报表系统 | 独立数据库避免影响主业务 |
数据迁移 | 新旧系统并行访问 |
📚 八、扩展建议
- 结合 AOP 实现基于注解的自动切换(如
@DataSource("slave")
) - 使用 MyBatis 时,只需将
SqlSessionFactory
指向dynamicDataSource
- 支持更多数据源:只需在
targetDataSources
中添加即可
🏁 九、总结
通过 DynamicDataSource
+ ThreadLocal
的组合,我们实现了 Spring Boot 项目中多数据源的 手动、灵活、线程安全的切换机制。
核心要点:
DataSourceContextHolder
提供上下文determineCurrentLookupKey()
返回当前数据源 keyAbstractRoutingDataSource
自动完成路由查找- 业务代码中手动设置并清理上下文
这种方式简单、可控,非常适合需要精确控制数据源的场景。