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