云计算、AI、云原生、大数据等一站式技术学习平台

网站首页 > 教程文章 正文

一文掌握 Spring Boot 数据库热插拔与多租户支持

jxf315 2025-09-13 02:05:19 教程文章 2 ℃

一文掌握 Spring Boot 数据库热插拔与多租户支持

一、概述

数据库热插拔功能允许应用程序在运行时动态添加、移除或切换数据源,而无需重启应用。这在 多租户系统、动态数据库管理和高可用性场景 中非常有用。

典型应用场景:

  • SaaS 多租户系统:根据租户动态加载对应数据库
  • 灰度/蓝绿发布:动态切换数据库实例
  • 高可用架构:主库挂了,自动切换到备用库
  • 数据库集群:动态扩容或缩容数据源

二、核心思路

  1. 使用 AbstractRoutingDataSource 实现动态路由
  2. 维护一个可动态更新的 数据源映射表
  3. 通过 线程上下文 传递当前使用的数据源标识
  4. 提供 管理接口(REST API)用于添加/移除数据源
  5. 结合 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
}

四、实战改进点

  1. 事务管理器绑定
@Bean
public PlatformTransactionManager txManager(DataSource ds) {
    return new DataSourceTransactionManager(ds);
}
  1. 数据源安全回收
    不直接删除,先标记“待回收”,延迟关闭连接池。
  2. 数据源健康检查
    定时探测可用性,异常时报警或切换备用库。
  3. 配置中心集成
    把数据源配置存到 Nacos/Apollo,支持集群环境下动态同步。
  4. 多租户自动切换
    通过 Request Header / JWT Token 自动识别租户 ID,不必写死在注解。

五、注意事项

  1. 连接池管理:及时关闭不再使用的 DataSource,避免内存泄漏
  2. 事务一致性:事务管理器必须基于 DynamicDataSource
  3. 性能:数据源数量过多会增加内存和线程开销
  4. 异常处理:动态切换失败时需保证系统回退到默认数据源
  5. 监控:建议结合 Micrometer + Prometheus 监控连接数、失败率

六、扩展功能

  • 数据源健康检查:定时任务检测
  • 负载均衡:多个相同配置的库之间轮询路由
  • 故障转移:主库不可用时自动切换备用库
  • 配置持久化:配置存储在数据库/配置中心,应用启动时自动加载

七、总结

通过以上方案,基于 Spring Boot 就能实现一个支持数据库热插拔的系统,具备以下能力:

  • 运行时动态管理数据源
  • 方法级别的数据源切换
  • 健康检查、故障转移
  • 配置中心集成,支持分布式

SaaS 多租户、动态扩容缩容、高可用架构 等场景下,这种方案非常实用。


最近发表
标签列表