springboot+dynamicDataSource怎么实现动态添加切换数据源

发布时间:2022-01-07 15:37:43 作者:iii
来源:亿速云 阅读:1266
# 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>

4.2 基础配置

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

五、核心实现

5.1 动态数据源注册

创建数据源注册工具类:

@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);
    }
}

5.2 数据源切换实现

5.2.1 注解方式切换

创建数据源注解:

@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());
        }
    }
}

5.2.2 编程式切换

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();
    }
}

5.3 动态添加数据源API

创建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;
}

六、高级功能实现

6.1 数据源健康检查

@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);
    }
}

6.2 数据源负载均衡

实现读数据源的轮询负载均衡:

@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);
    }
}

七、最佳实践

7.1 事务处理注意事项

在多数据源环境下,事务处理需要特别注意:

  1. 跨数据源事务:需要使用分布式事务解决方案如Seata
  2. 注解顺序@DS注解必须放在@Transactional之前
  3. 传播行为:避免在切换数据源的方法中使用REQUIRES_NEW

7.2 性能优化建议

  1. 连接池配置:为不同数据源配置合适的连接池参数
    
    spring:
     datasource:
       dynamic:
         datasource:
           master:
             hikari:
               maximum-pool-size: 20
               minimum-idle: 5
    
  2. 数据源预热:应用启动时初始化常用数据源连接
  3. 监控集成:集成Micrometer监控数据源指标

八、常见问题解决方案

8.1 数据源无法切换

现象:注解切换不生效,始终使用默认数据源

排查步骤: 1. 检查@DS注解是否被正确扫描 2. 确认切面执行顺序高于事务切面 3. 检查数据源名称是否拼写正确

8.2 动态添加的数据源不生效

可能原因: 1. 新数据源配置有误 2. 未正确刷新数据源集合

解决方案

// 添加数据源后手动刷新
routingDataSource.getDataSources().put(poolName, dataSource);
routingDataSource.afterPropertiesSet();

九、总结

本文详细介绍了在SpringBoot项目中实现动态数据源的全过程,包括:

  1. 动态数据源的基本原理和核心概念
  2. dynamic-datasource框架的集成方法
  3. 数据源动态注册和切换的实现细节
  4. 生产环境中的高级功能和最佳实践

通过本文的指导,开发者可以快速在项目中实现灵活的多数据源管理,满足复杂业务场景下的数据访问需求。


附录:完整配置示例

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

”`

推荐阅读:
  1. 如何实现SpringBoot Mybatis动态数据源切换方案
  2. Spring如何实现切换动态数据源

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

springboot dynamicdatasource

上一篇:SQL注入渗透测试以及护网面试题有哪些

下一篇:c++显式栈如何实现递归

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》