动态数据源与配置热加载
约 1657 字大约 6 分钟
布欧-Lewyon
2026-05-16
首页 › Spring Boot › 数据访问 › 动态数据源与配置热加载
生产环境中,数据库切换、读写分离、多租户隔离等场景要求在不重启 Spring 服务的前提下动态切换或更新数据源配置。Spring 通过 AbstractRoutingDataSource 和 @RefreshScope 提供了实现基础,但要做到生产可用还需要配合配置存储与健康检查。
核心方案对比
| 方案 | 热加载能力 | 复杂度 | 适用场景 |
|---|---|---|---|
AbstractRoutingDataSource + @Scheduled 轮询 | ✅ 秒级感知 | 低 | 读写分离、多租户 |
@RefreshScope + DataSource Bean | ✅ 手动触发 refresh | 中 | 配置项变更(URL/密码) |
动态 DataSource 重建 + @EventListener | ✅ 事件驱动 | 高 | 完全重连 |
| 中间件层(ProxySQL / ShardingSphere) | ✅ 无感 | 无需改代码 | 企业级数据库治理 |
AbstractRoutingDataSource 路由模式
AbstractRoutingDataSource 是 Spring 提供的抽象类,允许在运行时根据 lookupKey 选择目标 DataSource:
基本实现
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.get();
}
}public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void set(String dataSource) {
CONTEXT.set(dataSource);
}
public static String get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DynamicRoutingDataSource routingDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
DynamicRoutingDataSource routing = new DynamicRoutingDataSource();
Map<Object, Object> targets = new HashMap<>();
targets.put("master", master);
targets.put("slave", slave);
routing.setDefaultTargetDataSource(master); // 默认走 master
routing.setTargetDataSources(targets);
return routing;
}
}spring:
datasource:
master:
url: jdbc:postgresql://master-host:5432/db
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
slave:
url: jdbc:postgresql://slave-host:5432/db
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 10使用方式
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 写操作走 master
@Transactional
public User createUser(User user) {
return userRepository.save(user);
}
// 读操作走 slave(通过 AOP 自动切换)
public List<User> findAll() {
DynamicDataSourceContextHolder.set("slave");
try {
return userRepository.findAll();
} finally {
DynamicDataSourceContextHolder.clear();
}
}
}通过 AOP 自动切换(推荐):
@Aspect
@Component
public class DataSourceAspect {
@Around("@annotation(readOnly)")
public Object setReadOnly(ProceedingJoinPoint pjp, ReadOnly readOnly) throws Throwable {
DynamicDataSourceContextHolder.set("slave");
try {
return pjp.proceed();
} finally {
DynamicDataSourceContextHolder.clear();
}
}
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {}@ReadOnly
public List<User> findAll() {
return userRepository.findAll();
}配置热加载:不重启更新数据源
AbstractRoutingDataSource 路由模式解决了切换问题,但数据源的连接参数(URL、密码)本身是启动时加载的——如果需要修改密码或切换到一个全新的数据库实例,需要重建 DataSource。
方案一:@RefreshScope + 配置中心(需 Spring Cloud 或 Actuator)
Spring Cloud 的 @RefreshScope 可以让 Bean 在收到 RefreshScopeRefreshedEvent 后重新创建。
@RefreshScope
@Configuration
public class RefreshableDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
}触发刷新(需要 spring-boot-starter-actuator):
curl -X POST http://localhost:8080/actuator/refresh收到请求后,@RefreshScope 注解的 DataSource Bean 会被销毁并重新创建——新连接使用新配置。
方案二:数据库驱动配置(纯 Spring Boot,无需外部组件)
将数据源配置存储在配置表本身,通过 @Scheduled 定时轮询或事件监听热加载:
CREATE TABLE datasource_config (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE, -- master / slave1 / tenant_a
url VARCHAR(500) NOT NULL,
username VARCHAR(100) NOT NULL,
password VARCHAR(500) NOT NULL, -- 加密存储
driver_class VARCHAR(100),
max_pool_size INT DEFAULT 20,
enabled BOOLEAN DEFAULT TRUE,
updated_at TIMESTAMP NOT NULL
);@Component
public class DynamicDataSourceManager {
private final DynamicRoutingDataSource routingDataSource;
private final JdbcTemplate jdbcTemplate;
private final AtomicReference<Map<String, DataSource>> currentSources = new AtomicReference<>();
public DynamicDataSourceManager(DynamicRoutingDataSource routingDataSource,
JdbcTemplate jdbcTemplate) {
this.routingDataSource = routingDataSource;
this.jdbcTemplate = jdbcTemplate;
loadDataSources();
}
@Scheduled(fixedDelay = 15000) // 每 15 秒检查变更
public void refresh() {
// 检查配置表是否有更新
LocalDateTime lastUpdate = jdbcTemplate.queryForObject(
"SELECT MAX(updated_at) FROM datasource_config", LocalDateTime.class);
// 与实际比对(省略缓存的 checksum 比较逻辑)
loadDataSources();
}
private synchronized void loadDataSources() {
List<DataSourceConfig> configs = jdbcTemplate.query(
"SELECT * FROM datasource_config WHERE enabled = TRUE",
new BeanPropertyRowMapper<>(DataSourceConfig.class));
Map<Object, Object> targets = new HashMap<>();
for (DataSourceConfig cfg : configs) {
targets.put(cfg.name(), createDataSource(cfg));
}
routingDataSource.setTargetDataSources(targets);
routingDataSource.afterPropertiesSet(); // 重新初始化
log.info("数据源配置已更新: {}", configs.stream().map(DataSourceConfig::name).toList());
}
private DataSource createDataSource(DataSourceConfig cfg) {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(cfg.url());
hikariConfig.setUsername(cfg.username());
hikariConfig.setPassword(decrypt(cfg.password())); // 解密
hikariConfig.setMaximumPoolSize(cfg.maxPoolSize());
hikariConfig.setPoolName(cfg.name());
hikariConfig.setConnectionTimeout(5000);
return new HikariDataSource(hikariConfig);
}
}生产意识:轮询间隔不要过短(建议 15-30 秒),避免对配置表造成不必要的查询压力。更高效的做法:使用
application_version行版本号或发布/订阅模式(PostgreSQL LISTEN/NOTIFY、Redis Pub/Sub)替代轮询。
方案三:PostgreSQL LISTEN/NOTIFY 事件驱动(零轮询)
PostgreSQL 支持 LISTEN / NOTIFY,可以在配置表数据变更时主动通知应用:
@Component
public class PostgresNotifyListener {
@PostConstruct
public void listen() {
new Thread(() -> {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
stmt.execute("LISTEN datasource_channel");
conn.unwrap(PgConnection.class).getNotifications(); // 清除旧事件
while (!Thread.currentThread().isInterrupted()) {
// waitForNotification 阻塞等待 PG 通知
Notification[] notifications = conn.unwrap(PgConnection.class)
.getNotifications(30000);
if (notifications != null && notifications.length > 0) {
dynamicDataSourceManager.refresh();
}
}
} catch (Exception e) {
log.error("PG 监听异常", e);
}
}, "pg-notify-listener").start();
}
}-- 配置表更新时触发通知
CREATE OR REPLACE FUNCTION notify_datasource_change()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('datasource_channel', NEW.name || ' changed');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_datasource_config_change
AFTER INSERT OR UPDATE OR DELETE ON datasource_config
FOR EACH ROW EXECUTE FUNCTION notify_datasource_change();多租户数据库路由
SaaS 场景中,每个租户(客户)使用独立的数据库:
@Component
public class TenantDataSourceProvider {
private final Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();
public DataSource getTenantDataSource(String tenantId) {
return tenantDataSources.computeIfAbsent(tenantId, this::createTenantDataSource);
}
private DataSource createTenantDataSource(String tenantId) {
// 从租户注册表查询该租户的数据库配置
TenantConfig config = tenantConfigRepository.findByTenantId(tenantId);
HikariConfig hikari = new HikariConfig();
hikari.setJdbcUrl(config.getDbUrl());
hikari.setUsername(config.getDbUsername());
hikari.setPassword(decrypt(config.getDbPassword()));
hikari.setMaximumPoolSize(5);
hikari.setPoolName("tenant-" + tenantId);
return new HikariDataSource(hikari);
}
}数据源健康检查
动态数据源的一个关键挑战:如何知道数据源是否健康?
@Component
public class DataSourceHealthChecker {
@Scheduled(fixedRate = 30000) // 每 30 秒检查
public void checkAll() {
DynamicRoutingDataSource routing = (DynamicRoutingDataSource) applicationContext
.getBean("routingDataSource");
for (Entry<Object, Object> entry : routing.getTargetDataSources().entrySet()) {
String name = (String) entry.getKey();
DataSource ds = (DataSource) entry.getValue();
try (Connection conn = ds.getConnection()) {
if (!conn.isValid(3)) {
log.warn("数据源异常: {}", name);
// 可在此触发切换或通知
}
} catch (Exception e) {
log.error("数据源不可用: {}", name, e);
}
}
}
}小结
AbstractRoutingDataSource是动态数据源的核心,通过determineCurrentLookupKey()在运行时路由。- 配置热加载方案:
@RefreshScope + /actuator/refresh(适合配置中心)、数据库驱动定时轮询(适合纯 Spring Boot)、PostgreSQL LISTEN/NOTIFY(事件驱动零轮询)。 - 多租户场景:每个租户独立
DataSource,通过ConcurrentHashMap缓存。 - 动态重建
DataSource时务必注意:旧连接的优雅关闭(HikariDataSource.close())、afterPropertiesSet()调用、连接池泄露。 - 易错:
AbstractRoutingDataSource.setTargetDataSources()后必须调用afterPropertiesSet()才能生效。HikariCP 的连接池一旦创建不会自动感知 URL 变化——必须重建HikariDataSource。@RefreshScope注解的 Bean 在刷新时会销毁重建,如果DataSource在刷新时有正在执行的事务,事务会失败。数据库中加密存储密码时需要在应用层解密,不要在 SQL 查询中传输解密密钥。 - 思考任务:实现基于
AbstractRoutingDataSource的读写分离,主库写、从库读;将数据源配置存储到datasource_config表中,实现 15 秒轮询热加载;测试修改数据库密码后不重启服务是否生效。
上一节:MongoDB 集成
下一节:缓存
