You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot按请求URL动态切换数据库遇并发崩溃问题求助

问题分析

你的核心问题在于直接修改全局共享的DriverManagerDataSource配置,这是线程不安全的。并发请求时,多个线程会同时修改数据源的URL和Schema,导致请求之间互相干扰:比如请求A刚把数据源切换到db1,请求B立刻把它改成db2,此时A后续的数据库操作就会使用错误的数据源配置,最终抛出"No database selected"异常。

解决方案

以下提供两种线程安全的实现方案,分别适配JdbcTemplate和JPA场景:

方案一:基于JdbcTemplate的多数据源缓存实现

通过缓存每个db_name对应的独立DataSource和JdbcTemplate,避免全局修改,保证线程安全。

1. 创建数据源缓存管理类

import org.flywaydb.core.Flyway;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class DataSourceManager {
    private final Map<String, DataSource> dataSourceCache = new ConcurrentHashMap<>();
    private final Map<String, JdbcTemplate> jdbcTemplateCache = new ConcurrentHashMap<>();
    private final String urlBase;
    private final String username;
    private final String password;

    // 通过构造注入获取基础数据源配置
    public DataSourceManager(String urlBase, String username, String password) {
        this.urlBase = urlBase;
        this.username = username;
        this.password = password;
    }

    public JdbcTemplate getJdbcTemplate(String dbName) {
        return jdbcTemplateCache.computeIfAbsent(dbName, name -> {
            DataSource dataSource = createAndInitializeDataSource(name);
            return new JdbcTemplate(dataSource);
        });
    }

    private DataSource createAndInitializeDataSource(String dbName) {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(urlBase + dbName);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setSchema(dbName);

        // 执行Flyway迁移
        Flyway flyway = Flyway.configure()
                .dataSource(dataSource)
                .defaultSchema(dbName)
                .load();
        flyway.migrate();

        return dataSource;
    }
}

2. 在控制器中使用

@RestController
public class RestController {
    private final DataSourceManager dataSourceManager;

    public RestController(DataSourceManager dataSourceManager) {
        this.dataSourceManager = dataSourceManager;
    }

    @PostMapping("/{db_name}/update")
    public ResponseEntity<?> aggiornaDb(@PathVariable("db_name") String dbName) {
        JdbcTemplate jdbcTemplate = dataSourceManager.getJdbcTemplate(dbName);
        // 执行你的数据库操作,比如:
        jdbcTemplate.update("INSERT INTO statistiche_plu_giornaliere (...) VALUES (...)");
        return ResponseEntity.ok().build();
    }
}

方案二:基于JPA的AbstractRoutingDataSource动态切换

利用Spring提供的AbstractRoutingDataSource实现数据源动态路由,通过ThreadLocal隔离当前请求的数据源标识。

1. 定义数据源上下文

public class DataSourceContextHolder {
    private static final ThreadLocal<String> currentDbName = new ThreadLocal<>();

    public static void setCurrentDbName(String dbName) {
        currentDbName.set(dbName);
    }

    public static String getCurrentDbName() {
        return currentDbName.get();
    }

    public static void clear() {
        currentDbName.remove();
    }
}

2. 实现动态路由数据源

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getCurrentDbName();
    }
}

3. 配置数据源和Flyway初始化

import org.flywaydb.core.Flyway;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Configuration
public class DataSourceConfig {
    private final String urlBase;
    private final String username;
    private final String password;

    public DataSourceConfig(String urlBase, String username, String password) {
        this.urlBase = urlBase;
        this.username = username;
        this.password = password;
    }

    private final Map<String, DataSource> dataSourceCache = new ConcurrentHashMap<>();

    @Bean
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
        // 设置默认数据源(可选,比如连接到系统库)
        routingDataSource.setDefaultTargetDataSource(createDataSource("default"));
        
        // 初始化数据源映射(首次使用时会动态添加)
        Map<Object, Object> dataSourceMap = new HashMap<>();
        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }

    public DataSource getDataSource(String dbName) {
        return dataSourceCache.computeIfAbsent(dbName, this::createAndInitializeDataSource);
    }

    private DataSource createAndInitializeDataSource(String dbName) {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(urlBase + dbName);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setSchema(dbName);

        Flyway flyway = Flyway.configure()
                .dataSource(dataSource)
                .defaultSchema(dbName)
                .load();
        flyway.migrate();

        // 更新路由数据源的目标数据源映射
        DynamicRoutingDataSource routingDataSource = (DynamicRoutingDataSource) dynamicDataSource();
        routingDataSource.addTargetDataSource(dbName, dataSource);
        routingDataSource.afterPropertiesSet();

        return dataSource;
    }

    private DataSource createDataSource(String dbName) {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(urlBase + dbName);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    // 配置JPA实体管理器和事务管理器
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dynamicDataSource());
        em.setPackagesToScan("com.trei.statistiche_cloud.entity"); // 你的实体类包路径

        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);

        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "none"); // 因为用Flyway管理 schema
        properties.put("hibernate.show_sql", "true");
        em.setJpaPropertyMap(properties);

        return em;
    }

    @Bean
    public JpaTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }
}

4. 拦截器/控制器中设置数据源

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class DataSourceInterceptor implements HandlerInterceptor {
    private final DataSourceConfig dataSourceConfig;

    public DataSourceInterceptor(DataSourceConfig dataSourceConfig) {
        this.dataSourceConfig = dataSourceConfig;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String dbName = request.getRequestURI().split("/")[1]; // 从URL中提取db_name
        // 确保数据源已初始化
        dataSourceConfig.getDataSource(dbName);
        DataSourceContextHolder.setCurrentDbName(dbName);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        DataSourceContextHolder.clear();
    }
}

注册拦截器:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final DataSourceInterceptor dataSourceInterceptor;

    public WebConfig(DataSourceInterceptor dataSourceInterceptor) {
        this.dataSourceInterceptor = dataSourceInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(dataSourceInterceptor)
                .addPathPatterns("/{db_name}/**"); // 匹配包含db_name的请求路径
    }
}
关键说明
  • 两种方案都通过缓存每个db_name的独立数据源避免重复创建,同时保证线程安全。
  • 方案一适合JdbcTemplate场景,实现简单直接;方案二更适合JPA生态,利用Spring的动态数据源路由能力。
  • 避免直接修改全局数据源的配置,这是并发问题的根源。

内容的提问来源于stack exchange,提问作者simmons_vt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 21:18:49