dynamic-datasource 使用文档
本文档基于本项目(Spring Boot 4.1.1 + dynamic-datasource 4.5.0 + MyBatis-Plus 3.5.17)整理,涵盖依赖导入、配置、DataSourceManager 工具类、@DS 注解与编程式两种切换方式,以及其他常用扩展用法。
目录
- [1. 简介](#1. 简介)
- [2. Maven 依赖](#2. Maven 依赖)
- [3. properties 配置](#3. properties 配置)
- [4. DataSourceManager 工具类](#4. DataSourceManager 工具类)
- [5. 使用教程一:@DS 注解切换](#5. 使用教程一:@DS 注解切换)
- [6. 使用教程二:DataSourceManager 编程式切换](#6. 使用教程二:DataSourceManager 编程式切换)
- [7. 其他用法](#7. 其他用法)
- [8. 常见坑](#8. 常见坑)
1. 简介
dynamic-datasource 是苞米豆(baomidou)开源的多数据源解决方案,核心思路是:
- 容器中只存在一个
DynamicRoutingDataSource(路由数据源),内部持有一个Map<String, DataSource>; - 每次获取连接时,根据 ThreadLocal 上下文 (
DynamicDataSourceContextHolder)中保存的数据源名称,从 Map 中取出真实的DataSource返回; - 上下文是一个 栈结构(push / poll),天然支持嵌套切换和自动恢复。
因此它可以在运行时:
- 动态注册/移除数据源(往 Map 里 put / remove);
- 动态切换数据源(往 ThreadLocal 里 push 数据源名)。
2. Maven 依赖
Spring Boot 4.x 必须使用 dynamic-datasource-spring-boot4-starter(对应 Spring Boot 2/3 分别是 spring-boot-starter / spring-boot3-starter):
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot4-starter</artifactId>
<version>4.5.0</version>
</dependency>
<!-- 可选:MyBatis-Plus,本文档示例基于其 BaseMapper 查询 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>3.5.17</version>
</dependency>
<!-- 可选:MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
引入 starter 后自动配置即生效,无需任何注解或 @Configuration。
3. properties 配置
引入 starter 后,原生的 spring.datasource.* 配置会被接管,必须迁移到 spring.datasource.dynamic.* 前缀下,并且必须至少配置一个名为 master 的主数据源(primary 指向它):
properties
spring.application.name=demo
# ---------- dynamic-datasource 核心配置 ----------
# 未指定数据源时使用的默认数据源名
spring.datasource.dynamic.primary=master
# 严格匹配模式:设为 true 时,切换到不存在的数据源会抛异常(false 时回退到 primary)
spring.datasource.dynamic.strict=false
# 主数据源 master(名字可自定义,但必须与 primary 一致)
spring.datasource.dynamic.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.master.url=jdbc:mysql://localhost:3306/demo?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
spring.datasource.dynamic.datasource.master.username=root
spring.datasource.dynamic.datasource.master.password=root
# ---------- MyBatis-Plus ----------
mybatis-plus.mapper-locations[0]=classpath*:/mapper/**/*.xml
mybatis-plus.type-aliases-package=org.gudian.user
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
配置多个静态数据源(扩展)
如果数据源在编码期就已知,可直接在 properties 中并列配置多个(本项目是运行时动态注册,故只配 master):
properties
spring.datasource.dynamic.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.slave.url=jdbc:mysql://localhost:3306/demo2?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
spring.datasource.dynamic.datasource.slave.username=root
spring.datasource.dynamic.datasource.slave.password=root
常用可选配置
| 配置项 | 说明 |
|---|---|
spring.datasource.dynamic.strict |
严格模式。true 时查找不到数据源抛异常,false 时回退 primary |
spring.datasource.dynamic.datasource.<name>.lazy |
该数据源是否懒加载(默认启动时立即初始化连接池) |
spring.datasource.dynamic.datasource.<name>.pool-type |
连接池类型,默认 HikariCP(也支持 Druid、Dbcp2 等,需另引依赖) |
spring.datasource.dynamic.seata |
是否开启 Seata 分布式事务支持 |
4. DataSourceManager 工具类
DataSourceManager 是本项目对 dynamic-datasource 底层 API 的封装,提供了线程安全的编程式切换 (自动切换、自动恢复)和运行时动态注册/移除数据源能力。
完整代码见 DataSourceManager.java:
java
package org.gudian.datasource;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.gudian.datasource.exception.DataSourceAlreadyExistsException;
import org.gudian.datasource.exception.DataSourceNotFoundException;
import org.gudian.datasource.exception.DataSourceRegisterException;
import org.gudian.datasource.exception.IllegalDataSourceOperationException;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Set;
import java.util.function.Supplier;
@Component
public class DataSourceManager {
/** 主数据源名,与 spring.datasource.dynamic.primary 保持一致 */
public static final String PRIMARY_DATA_SOURCE = "master";
private final DynamicRoutingDataSource routingDataSource;
private final DefaultDataSourceCreator dataSourceCreator;
public DataSourceManager(DataSource dataSource, DefaultDataSourceCreator dataSourceCreator) {
// 自动配置中 dataSource 的声明类型为 javax.sql.DataSource,实际为 DynamicRoutingDataSource,
// 直接按具体类型注入会因声明类型不匹配而找不到 bean,因此注入后强转
if (!(dataSource instanceof DynamicRoutingDataSource routing)) {
throw new IllegalStateException("当前 DataSource 不是 DynamicRoutingDataSource,无法进行动态数据源管理");
}
this.routingDataSource = routing;
this.dataSourceCreator = dataSourceCreator;
}
/**
* 编程式切换:在指定数据源上执行 Runnable,执行完自动恢复原数据源
*/
public void executeInDs(String dsName, Runnable action) {
DynamicDataSourceContextHolder.push(dsName);
try {
action.run();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
/**
* 编程式切换:在指定数据源上执行 Supplier 并返回结果,执行完自动恢复原数据源
*/
public <T> T executeInDs(String dsName, Supplier<T> action) {
DynamicDataSourceContextHolder.push(dsName);
try {
return action.get();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
/**
* 运行时注册数据源(不持久化,重启后失效)
*/
public void register(String name, String url, String username, String password) {
if (isBlank(name) || isBlank(url) || isBlank(username) || isBlank(password)) {
throw new IllegalDataSourceOperationException("name、url、username、password 均不能为空");
}
if (contains(name)) {
throw new DataSourceAlreadyExistsException(name);
}
DataSourceProperty property = new DataSourceProperty();
property.setPoolName(name);
property.setUrl(url);
property.setUsername(username);
property.setPassword(password);
property.setDriverClassName("com.mysql.cj.jdbc.Driver");
DataSource created = null;
try {
created = dataSourceCreator.createDataSource(property);
try (Connection connection = created.getConnection()) {
// 能成功获取连接即视为数据源可用(连接验证,连不上会抛异常)
}
} catch (Exception e) {
closeQuietly(created);
throw new DataSourceRegisterException(name, e);
}
routingDataSource.addDataSource(name, created);
}
/**
* 移除数据源(主数据源 master 不允许移除)
*/
public void remove(String name) {
if (PRIMARY_DATA_SOURCE.equals(name)) {
throw new IllegalDataSourceOperationException("主数据源不允许移除: " + name);
}
if (!contains(name)) {
throw new DataSourceNotFoundException(name);
}
routingDataSource.removeDataSource(name);
}
/** 数据源是否已存在 */
public boolean contains(String name) {
return routingDataSource.getDataSources().containsKey(name);
}
/** 列出所有已注册的数据源名 */
public Set<String> list() {
return Set.copyOf(routingDataSource.getDataSources().keySet());
}
private void closeQuietly(DataSource dataSource) {
if (dataSource instanceof AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception ignored) {
// 注册失败后的资源清理失败可忽略
}
}
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
}
设计要点说明
| 要点 | 说明 |
|---|---|
构造器注入 DataSource 后强转 |
自动配置注册的 dataSource bean 声明类型是 javax.sql.DataSource 父类型,按 DynamicRoutingDataSource 具体类型注入会报找不到 bean,必须注入父类型后 instanceof 强转 |
push / poll 配对 |
push 压栈 → 执行 → finally 中 poll 出栈。即使业务代码抛异常,finally 也保证恢复,不会污染线程(Tomcat 线程池复用线程) |
| 嵌套调用安全 | 栈结构支持嵌套:executeInDs("a", () -> executeInDs("b", ...)),内层执行完回到 a,外层执行完回到最初 |
| 注册时验证连接 | createDataSource 后先 getConnection() 试连,连不上则关闭连接池并抛 DataSourceRegisterException,避免注册进无效数据源 |
| 注册不持久化 | 数据源只存于内存 Map,应用重启后动态注册的数据源消失(本项目的需求即如此) |
5. 使用教程一:@DS 注解切换
@DS("数据源名") 是最常用的声明式切换方式,基于 AOP 实现,可标注在方法 或类上(方法优先于类)。
5.1 基本用法
java
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
// 方法级注解:本方法走 slave 数据源
@DS("slave")
public List<Order> queryFromSlave() {
return orderMapper.selectList(null);
}
// 未标注:走 primary(master)数据源
public List<Order> queryFromMaster() {
return orderMapper.selectList(null);
}
}
5.2 类级注解(读写分离典型用法)
java
// 整个 Service 默认走 slave,个别方法可再覆盖
@DS("slave")
@Service
public class ReportService {
public void generate() { /* 走 slave */ }
@DS("master") // 方法注解覆盖类注解
public void save() { /* 走 master */ }
}
5.3 @DS("master") 与 @Master
切到其他数据源后想明确回到主库,除 @DS("master") 外还可以用框架提供的 @Master 注解,语义更清晰:
java
@Master
public void writeOnMaster() { /* 走 primary 数据源 */ }
5.4 注意事项
- 同类内部自调用失效 :
@DS是 AOP 代理实现的,this.methodB()不会走代理,注解不生效。解决方式:注入自身代理、拆到另一个 Bean、或改用DataSourceManager.executeInDs; - 不要与编程式切换混用在同一方法 :
executeInDs的 push 压在@DS之上时,以executeInDs的为准(内层覆盖外层),容易造成误解; - 注解值必须是已存在 的数据源名(静态配置的或已动态注册的),不存在时依
strict配置决定抛异常还是回退 primary。
6. 使用教程二:DataSourceManager 编程式切换
适用于运行时才能确定数据源的场景(如按租户路由、用户传入数据源名等)。本项目即采用此方案。
6.1 注入并使用
java
@Service
@RequiredArgsConstructor
public class UserQueryService {
private final UserMapper userMapper;
private final DataSourceManager dataSourceManager;
public List<User> queryUsers(String dsName) {
// 1. 先校验数据源存在
if (!dataSourceManager.contains(dsName)) {
throw new DataSourceNotFoundException(dsName);
}
// 2. 在指定数据源上执行查询,执行完自动恢复
return dataSourceManager.executeInDs(dsName, () -> userMapper.selectList(null));
}
}
无返回值的操作用 Runnable 重载:
java
dataSourceManager.executeInDs("db2", () -> userMapper.insert(new User()));
6.2 动态注册/移除数据源(REST API)
本项目通过 DataSourceController.java 暴露了管理接口:
| 接口 | 方法 | 说明 |
|---|---|---|
/api/datasource |
POST | 注册数据源,body:{"name":"db2","url":"jdbc:mysql://...","username":"root","password":"root"} |
/api/datasource |
GET | 列出所有数据源名 |
/api/datasource/{name} |
DELETE | 移除数据源(master 受保护) |
注册后会立即试连验证,连不上返回 400。
6.3 完整验证流程(curl)
bash
# 1. 查询主库
curl "http://localhost:8080/api/users?ds=master"
# [{"age":20,"id":1,"name":"alice"}]
# 2. 动态注册指向 demo2 库的数据源 db2
curl -X POST "http://localhost:8080/api/datasource" \
-H "Content-Type: application/json" \
-d '{"name":"db2","url":"jdbc:mysql://localhost:3306/demo2?characterEncoding=utf8&useSSL=false&serverTimezone=UTC","username":"root","password":"root"}'
# {"message":"数据源注册成功: db2"}
# 3. 切换到 db2 查询
curl "http://localhost:8080/api/users?ds=db2"
# [{"age":30,"id":1,"name":"bob"}]
# 4. 查看数据源列表
curl "http://localhost:8080/api/datasource"
# ["db2","master"]
# 5. 移除 db2
curl -X DELETE "http://localhost:8080/api/datasource/db2"
7. 其他用法
7.1 直接使用底层 API(不经过 DataSourceManager)
如果不想封装,也可以直接操作 DynamicDataSourceContextHolder,但必须自己保证 push/poll 配对(推荐 try-finally):
java
DynamicDataSourceContextHolder.push("db2");
try {
// 此区间内的所有 MyBatis/事务操作都走 db2
List<User> users = userMapper.selectList(null);
} finally {
DynamicDataSourceContextHolder.poll(); // 出栈恢复,切勿用 clear()
}
clear() 会清空整个栈,在嵌套场景会破坏外层的数据源状态,一般只在请求拦截器收尾兜底时使用。
7.2 跨数据源事务:@DSTransactional
Spring 原生 @Transactional 开启后会绑定单个连接,事务内切换数据源不再生效 。若需要跨库操作且各自独立提交/回滚,使用框架提供的 @DSTransactional:
java
@DSTransactional
public void crossDbWrite() {
userMapper.insert(userA); // 走 master
slaveMapper.insert(userB); // 走 slave(需 @DS("slave") 或编程式切换)
// 任意一步抛异常,已执行的操作全部回滚
}
注意 @DSTransactional 是"多数据源各自事务 + 最终一致回滚",不是真正的分布式强一致事务;后者需引入 Seata。
7.3 嵌套切换(栈机制示例)
java
dataSourceManager.executeInDs("db1", () -> {
// 此处走 db1
dataSourceManager.executeInDs("db2", () -> {
// 此处走 db2(内层覆盖外层)
});
// 此处恢复走 db1
});
// 此处恢复走 primary
7.4 请求级拦截器切换(扩展思路)
可以自定义拦截器从请求头解析租户/数据源名,请求开始时 push、结束时 poll,实现业务代码零感知的切换:
java
@Component
public class DsInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String ds = request.getHeader("X-DataSource");
if (ds != null) {
DynamicDataSourceContextHolder.push(ds);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
DynamicDataSourceContextHolder.poll();
}
}
7.5 编程式添加静态配置数据源的等价操作
register() 内部使用的 DefaultDataSourceCreator.createDataSource(DataSourceProperty) 也可以用于读取自定义配置(如数据库里的租户连接信息表)批量初始化数据源,DataSourceProperty 常用属性:
| 属性 | 说明 |
|---|---|
poolName |
连接池/数据源名 |
url / username / password |
连接信息 |
driverClassName |
驱动类(可省略,框架会从 url 推断) |
lazy |
是否懒加载 |
initMethod / fault |
初始化方法 / 是否为兜底数据源 |
7.6 动态数据源监控
DynamicRoutingDataSource.getDataSources() 返回 Map<String, DataSource>,可用于自检、健康检查或对接 Spring Boot Actuator(引入 actuator 后 dynamic-datasource 自动注册连接池指标)。
8. 常见坑
| 坑 | 现象 | 解决 |
|---|---|---|
按 DynamicRoutingDataSource 具体类型注入失败 |
启动报 No qualifying bean of type 'DynamicRoutingDataSource' |
自动配置 bean 声明类型是 javax.sql.DataSource,注入父类型后强转(见 DataSourceManager 构造器) |
@Transactional 内切换数据源无效 |
事务内查询始终走事务开启时绑定的库 | 事务开启前切换,或改用 @DSTransactional |
同类自调用 @DS 失效 |
AOP 代理不拦截 this.xxx() |
拆 Bean、注入自身代理、或用 executeInDs |
| 动态注册的数据源重启后消失 | 数据源只存于内存 | 如需持久化,自行保存连接信息并在启动时(如 ApplicationRunner)重新 register |
| 切换到不存在的数据源 | strict=false 静默回退 primary,可能导致查错库 | 生产建议 spring.datasource.dynamic.strict=true |
| push 后忘记 poll | 线程池复用线程,后续请求拿到脏数据源 | 始终用 try-finally 或直接使用 DataSourceManager.executeInDs |