您好,登录后才能下订单哦!
密码登录
登录注册
点击 登录注册 即表示同意《亿速云用户服务条款》
# 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进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。