动态添加/移除数据源
基础介绍
多租户场景里常见这样的需求:新租户进来时需要在不重启应用的前提下增加一个数据源,租户注销时再把它摘掉。
DynamicRoutingDataSource 为此提供了 addDataSource / removeDataSource 两个方法,配合 DefaultDataSourceCreator 就能在运行期创建任意连接池。
使用步骤
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.creator.jndi.JndiDataSourceCreator;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.sql.DataSource;
import java.util.Set;
@RestController
@RequestMapping("/datasources")
public class DataSourceController {
private final DataSource dataSource;
private final DefaultDataSourceCreator dataSourceCreator;
private final JndiDataSourceCreator jndiDataSourceCreator;
public DataSourceController(DataSource dataSource,
DefaultDataSourceCreator dataSourceCreator,
JndiDataSourceCreator jndiDataSourceCreator) {
this.dataSource = dataSource;
this.dataSourceCreator = dataSourceCreator;
this.jndiDataSourceCreator = jndiDataSourceCreator;
}
@GetMapping
public Set<String> now() {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
return ds.getDataSources().keySet();
}
/**
* 通用添加(推荐):由 DefaultDataSourceCreator 根据环境自动选择连接池。
* 选择顺序:druid > hikaricp > beecp > dbcp2 > basic
*/
@PostMapping("/add")
public Set<String> add(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty property = new DataSourceProperty();
BeanUtils.copyProperties(dto, property);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource newDataSource = dataSourceCreator.createDataSource(property);
ds.addDataSource(dto.getPoolName(), newDataSource);
return ds.getDataSources().keySet();
}
@PostMapping("/addJndi")
public Set<String> addJndi(String poolName, String jndiName) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource newDataSource = jndiDataSourceCreator.createDataSource(jndiName);
ds.addDataSource(poolName, newDataSource);
return ds.getDataSources().keySet();
}
@DeleteMapping
public String remove(String name) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
ds.removeDataSource(name);
return "删除成功";
}
}
DefaultDataSourceCreator 按环境自动挑选连接池,并且会自动处理懒加载、ENC() 解密与初始化脚本。
各个具体的创建器(DruidDataSourceCreator、HikariDataSourceCreator 等)虽然也可以单独注入使用,但它们绕过了上述统一处理,只建议在明确知道原因时使用。
行为细节
addDataSource(String name, DataSource dataSource):
- 以
name为 key 覆盖写入(同名会替换旧数据源) - 按命名规则把新数据源自动归入对应分组——加入
slave_3会自动进slave组,成为负载均衡的一个成员 - 方法带
synchronized,并发安全
removeDataSource(String name):
- 名称不能为空
- 组内成员被移除后为空时,该组会被自动清理
- 同样带
synchronized
注意事项
removeDataSource 只是把数据源从路由表中摘掉,它不会等你在途的请求结束。
如果此刻正好有线程拿到这个数据源准备取连接,就会拿到 null 并抛出 NullPointerException(严格模式下也拦不住这类竞态)。
生产环境的正确做法是分两步:
- 先摘流量:把该库从分组中移出(例如换个名字让
slave组不再包含它),等待在途请求结束 - 再调用移除:确认没有活跃连接后执行
removeDataSource
也可以考虑开启 grace-destroy,让连接池在关闭前等待活跃连接归还。
其他两点:
- 运行期新增的数据源同样会被 Actuator 端点统计到,可用它验证是否添加成功,见运维端点与指标。
- 数据源清单如果来自外部(配置中心、引导库),更适合实现
DynamicDataSourceProvider,见自定义数据源来源。
常见问题
集群/多实例部署,是不是每台机器都要 add?
是。addDataSource 只作用于当前 JVM(#233)。常见方案有三种:
- 每台实例启动时从同一个来源(引导库 / 配置中心)加载全量清单,运行期不再动态增删;
- 管理操作通过 MQ 或发布订阅广播到所有实例;
- 把数据源配置整体交给配置中心,自行接入刷新(框架本身不内置刷新能力)。
怎么让「数据源清单」本身存库?
继承 AbstractJdbcDataSourceProvider,从引导库查出 Map<String, DataSourceProperty> 返回即可(#107、#302)。引导库自身的连接信息仍然写在配置文件里——这也是「引导」二字的含义。更完整的扩展说明见自定义数据源来源。
引导库里没有数据时启动会报错吗?
自定义 provider 返回 null 或空 Map,在旧版本会直接 NPE(#302);当前版本已兼容 null(启动后得到空清单并打印警告日志)。自己写 provider 时仍建议对空结果做兜底。
运行中重复 add 同名数据源会怎样?
同名会替换:旧池被关闭,新池生效。可用于「换库不停机」的滚动切换。
add/remove 与正在执行的请求冲突怎么办?
两个方法都是 synchronized 的,路由侧对「删除瞬间的并发读」也做了空值防护(不会把 null 连接交给调用方)。但业务上仍应按上面的注意事项先摘流量再移除。
源码分析
DataSourceCreator 是创建器的统一抽象,DefaultDataSourceCreator 按注册顺序取第一个 support() 返回 true 的实现:
public interface DataSourceCreator {
/**
* 通过属性创建数据源
*
* @param dataSourceProperty 数据源属性
* @return 被创建的数据源
*/
DataSource createDataSource(DataSourceProperty dataSourceProperty);
/**
* 当前创建器是否支持根据此属性创建
*
* @param dataSourceProperty 数据源属性
* @return 是否支持
*/
boolean support(DataSourceProperty dataSourceProperty);
}
BasicDataSourceCreator使用 Spring 原生的创建方式,只支持最基础配置,且不做池化。JndiDataSourceCreator比较特殊:直接按jndiName从容器查找,不参与support()自动选择链。- 其余实现分别对应 Druid / HikariCP / BeeCP / DBCP2 / C3P0 / Oracle UCP / Atomikos。