网站首页 > 教程文章 正文
一文掌握 Spring Boot 数据库热插拔与多租户支持
一、概述
数据库热插拔功能允许应用程序在运行时动态添加、移除或切换数据源,而无需重启应用。这在 多租户系统、动态数据库管理和高可用性场景 中非常有用。
典型应用场景:
- SaaS 多租户系统:根据租户动态加载对应数据库
- 灰度/蓝绿发布:动态切换数据库实例
- 高可用架构:主库挂了,自动切换到备用库
- 数据库集群:动态扩容或缩容数据源
二、核心思路
- 使用 AbstractRoutingDataSource 实现动态路由
- 维护一个可动态更新的 数据源映射表
- 通过 线程上下文 传递当前使用的数据源标识
- 提供 管理接口(REST API)用于添加/移除数据源
- 结合 AOP 注解实现方法级别的数据源切换
三、实现步骤
3.1 添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
</dependencies>
3.2 动态数据源配置
// 数据源上下文持有者
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setDataSource(String key) { CONTEXT.set(key); }
public static String getDataSource() { return CONTEXT.get(); }
public static void clear() { CONTEXT.remove(); }
}
// 动态数据源路由
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
@Configuration
public class DataSourceConfig {
private final Map<Object, Object> dataSourceMap = new ConcurrentHashMap<>();
@Bean
@Primary
public DataSource dynamicDataSource() {
DynamicDataSource ds = new DynamicDataSource();
ds.setDefaultTargetDataSource(createDefaultDataSource());
ds.setTargetDataSources(dataSourceMap);
return ds;
}
private DataSource createDefaultDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setJdbcUrl("jdbc:mysql://localhost:3306/default_db");
ds.setUsername("root");
ds.setPassword("password");
return ds;
}
public void addDataSource(String key, DataSource dataSource) {
dataSourceMap.put(key, dataSource);
refreshDynamicDataSource();
}
public void removeDataSource(String key) {
if (dataSourceMap.containsKey(key)) {
DataSource ds = (DataSource) dataSourceMap.remove(key);
if (ds instanceof HikariDataSource) ((HikariDataSource) ds).close();
refreshDynamicDataSource();
}
}
private void refreshDynamicDataSource() {
DynamicDataSource ds = SpringContextUtil.getBean(DynamicDataSource.class);
ds.setTargetDataSources(dataSourceMap);
ds.afterPropertiesSet();
}
public Map<Object, Object> getDataSourceMap() {
return dataSourceMap;
}
}
3.3 数据源管理服务
@Service
public class DataSourceManagementService {
@Autowired
private DataSourceConfig dataSourceConfig;
public boolean addDataSource(String key, DataSourceProperties props) {
try {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl(props.getUrl());
ds.setUsername(props.getUsername());
ds.setPassword(props.getPassword());
ds.setDriverClassName(props.getDriverClassName());
ds.setMaximumPoolSize(props.getMaxPoolSize());
ds.setMinimumIdle(2);
ds.setConnectionTimeout(30000);
ds.setIdleTimeout(600000);
ds.setMaxLifetime(1800000);
try (Connection c = ds.getConnection()) { } // 测试连接
dataSourceConfig.addDataSource(key, ds);
return true;
} catch (Exception e) {
return false;
}
}
public boolean removeDataSource(String key) {
try {
dataSourceConfig.removeDataSource(key);
return true;
} catch (Exception e) {
return false;
}
}
public List<String> getActiveDataSources() {
return new ArrayList<>(dataSourceConfig.getDataSourceMap().keySet());
}
}
@Data
public class DataSourceProperties {
private String url;
private String username;
private String password;
private String driverClassName = "com.mysql.cj.jdbc.Driver";
private int maxPoolSize = 10;
}
3.4 控制器接口
@RestController
@RequestMapping("/api/datasource")
public class DataSourceController {
@Autowired
private DataSourceManagementService service;
@PostMapping("/add")
public ResponseEntity<String> addDataSource(@RequestParam String key,
@RequestBody DataSourceProperties props) {
return service.addDataSource(key, props) ?
ResponseEntity.ok("数据源添加成功") :
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("数据源添加失败");
}
@DeleteMapping("/remove/{key}")
public ResponseEntity<String> removeDataSource(@PathVariable String key) {
return service.removeDataSource(key) ?
ResponseEntity.ok("数据源移除成功") :
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("数据源移除失败");
}
@GetMapping("/list")
public ResponseEntity<List<String>> listDataSources() {
return ResponseEntity.ok(service.getActiveDataSources());
}
}
3.5 AOP 实现数据源切换
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.example.annotation.TargetDataSource)")
public void pointcut() {}
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
TargetDataSource ds = method.getAnnotation(TargetDataSource.class);
if (ds != null) DataSourceContextHolder.setDataSource(ds.value());
try {
return pjp.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String value();
}
3.6 使用示例
@Service
public class UserService {
@Autowired private UserRepository repo;
@TargetDataSource("tenant1")
public List<User> getUsersForTenant1() { return repo.findAll(); }
@TargetDataSource("tenant2")
public List<User> getUsersForTenant2() { return repo.findAll(); }
}
动态添加数据源示例:
POST /api/datasource/add?key=tenant3
Content-Type: application/json
{
"url": "jdbc:mysql://localhost:3306/tenant3_db",
"username": "tenant3_user",
"password": "tenant3_password",
"driverClassName": "com.mysql.cj.jdbc.Driver",
"maxPoolSize": 15
}
四、实战改进点
- 事务管理器绑定
@Bean
public PlatformTransactionManager txManager(DataSource ds) {
return new DataSourceTransactionManager(ds);
}
- 数据源安全回收
不直接删除,先标记“待回收”,延迟关闭连接池。 - 数据源健康检查
定时探测可用性,异常时报警或切换备用库。 - 配置中心集成
把数据源配置存到 Nacos/Apollo,支持集群环境下动态同步。 - 多租户自动切换
通过 Request Header / JWT Token 自动识别租户 ID,不必写死在注解。
五、注意事项
- 连接池管理:及时关闭不再使用的 DataSource,避免内存泄漏
- 事务一致性:事务管理器必须基于 DynamicDataSource
- 性能:数据源数量过多会增加内存和线程开销
- 异常处理:动态切换失败时需保证系统回退到默认数据源
- 监控:建议结合 Micrometer + Prometheus 监控连接数、失败率
六、扩展功能
- 数据源健康检查:定时任务检测
- 负载均衡:多个相同配置的库之间轮询路由
- 故障转移:主库不可用时自动切换备用库
- 配置持久化:配置存储在数据库/配置中心,应用启动时自动加载
七、总结
通过以上方案,基于 Spring Boot 就能实现一个支持数据库热插拔的系统,具备以下能力:
- 运行时动态管理数据源
- 方法级别的数据源切换
- 健康检查、故障转移
- 配置中心集成,支持分布式
在 SaaS 多租户、动态扩容缩容、高可用架构 等场景下,这种方案非常实用。
- 上一篇: dify开启多租户模式_多租户权限
- 下一篇: springboot多租户实现(1):基础架构
猜你喜欢
- 2025-09-13 SDN、NFV、云网融合傻傻分不清?理清它们的来龙去脉与应用逻辑
- 2025-09-13 Java多租户SaaS系统实现方案_多租户 springboot
- 2025-09-13 【推荐】一款开源免费、功能强大的多租户 Saas 快速开发平台
- 2025-09-13 EFCore多租户实现-共享数据库模式
- 2025-09-13 这款 .NET 9 + React 后台权限管理系统太强了!支持多租户、按钮权限(简单易用且文档齐全)
- 2025-09-13 设计一个多租户 SaaS 系统,如何实现租户数据隔离(...
- 2025-09-13 “凭什么要比业主多收钱?”租户抱怨遭区别对待,小区管理方喊委屈……
- 2025-09-13 设计一个多租户 SaaS 系统,如何实现租户数据隔离与资源配额控制?
- 2025-09-13 多租户登录页设计全攻略:如何实现灵活切换与数据隔离?
- 2025-09-13 springboot多租户实现(1):基础架构
- 最近发表
-
- K8s 部署频繁出错?一套流程教你快速定位故障,工作效率翻倍
- 防火墙服务无法启用,显示灰色的解决办法
- 网络问题-电脑无法上网处理思路以及办法 (总集)
- Win10学院:Windows Denfender无法启动怎么办?
- Windows账户登录问题解决方案_登录windows账户什么意思
- IIS无法启动提示计算机上"."的服务W3SVC,一分钟搞定,抓紧收藏
- 已申请到免费顶级域名如何管理_顶级域名免费注册
- 网站被劫持了老是跳转怎么办_网站被劫持到其它网站如何解决
- 这些“常用药”被注销!涉及维生素、去痛片、眼药水等!快看看你家有吗?
- 《皕宋楼藏书志》清 藏书家陆心源与其门人李宗莲合编的藏书目录
- 标签列表
-
- location.href (44)
- document.ready (36)
- git checkout -b (34)
- 跃点数 (35)
- 阿里云镜像地址 (33)
- qt qmessagebox (36)
- mybatis plus page (35)
- vue @scroll (38)
- 堆栈区别 (33)
- 什么是容器 (33)
- sha1 md5 (33)
- navicat导出数据 (34)
- 阿里云acp考试 (33)
- 阿里云 nacos (34)
- redhat官网下载镜像 (36)
- srs服务器 (33)
- pico开发者 (33)
- https的端口号 (34)
- vscode更改主题 (35)
- 阿里云资源池 (34)
- os.path.join (33)
- redis aof rdb 区别 (33)
- 302跳转 (33)
- http method (35)
- js array splice (33)