spring配置双数据源

Spring 双数据源配置与动态切换全指南

在企业级开发中,双数据源(或多数据源)是常见需求,例如 “主业务数据存库 A,特殊业务数据存库 B”。Spring 提供了 AbstractRoutingDataSource 抽象类实现数据源动态路由,配合 AOP 可实现 “注解式切换数据源”。从 “数据源配置→动态路由→AOP 切换→事务注意事项” 四个维度,详解双数据源的实现原理与最佳实践。

双数据源核心原理

AbstractRoutingDataSource

Spring 动态数据源的核心是 AbstractRoutingDataSource(抽象路由数据源),其工作流程如下:

  1. 维护数据源映射:内部通过 targetDataSources 存储 “数据源 Key → 数据源实例” 的映射,通过 defaultTargetDataSource 指定默认数据源;
  2. 动态路由:当执行数据库操作时,调用 determineCurrentLookupKey() 方法获取当前数据源 Key;
  3. 获取数据源:根据 Key 从 targetDataSources 中匹配对应的数据源,若无匹配则使用默认数据源。

简单来说,AbstractRoutingDataSource 相当于一个 “数据源路由器”,通过 Key 决定当前使用哪个数据源。

双数据源配置步骤(XML 方式)

以 Druid 连接池为例,完整配置包括 “基础数据源配置→动态数据源配置→连接池与事务配置”。

1. 步骤 1:引入依赖(Maven)

需引入 Druid 连接池、Spring JDBC、事务、AOP 相关依赖:

<!-- Druid 连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.16</version>
</dependency>

<!-- Spring JDBC(数据源管理) -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.3.29.RELEASE</version>
</dependency>

<!-- Spring 事务 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>4.3.29.RELEASE</version>
</dependency>

<!-- Spring AOP(切面切换数据源) -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.3.29.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

2. 步骤 2:配置基础数据源(Druid)

配置两个独立的 Druid 数据源(分别对应 “主库 video” 和 “特殊库 ad”),并通过外部属性文件注入连接信息(避免硬编码)。

(1)外部属性文件(db.properties)
# 主数据源(video 库)
video.url=jdbc:mysql://localhost:3306/video?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
video.username=root
video.password=123456

# 特殊数据源(ad 库)
ad.url=jdbc:mysql://localhost:3306/ad?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
ad.username=root
ad.password=123456
(2)XML 配置数据源
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 1. 引入外部属性文件 -->
    <context:property-placeholder location="classpath:db.properties"/>

    <!-- 2. 配置主数据源(video 库) -->
    <bean id="videoDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!-- 数据库连接信息 -->
        <property name="url" value="${video.url}"/>
        <property name="username" value="${video.username}"/>
        <property name="password" value="${video.password}"/>

        <!-- Druid 连接池参数优化 -->
        <property name="initialSize" value="5"/>         <!-- 初始化连接数 -->
        <property name="maxActive" value="20"/>          <!-- 最大活跃连接数 -->
        <property name="minIdle" value="5"/>             <!-- 最小空闲连接数 -->
        <property name="maxWait" value="3000"/>          <!-- 获取连接超时时间(毫秒) -->
        <property name="validationQuery" value="SELECT 1"/> <!-- 连接有效性检测 SQL -->
        <property name="testWhileIdle" value="true"/>    <!-- 空闲时检测连接有效性 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 检测间隔(毫秒) -->
        <property name="minEvictableIdleTimeMillis" value="300000"/> <!-- 连接最小生存时间(毫秒) -->
    </bean>

    <!-- 3. 配置特殊数据源(ad 库) -->
    <bean id="adDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <property name="url" value="${ad.url}"/>
        <property name="username" value="${ad.username}"/>
        <property name="password" value="${ad.password}"/>
        <!-- 连接池参数与主数据源一致,可复用 -->
        <property name="initialSize" value="5"/>
        <property name="maxActive" value="20"/>
        <property name="minIdle" value="5"/>
        <property name="maxWait" value="3000"/>
        <property name="validationQuery" value="SELECT 1"/>
        <property name="testWhileIdle" value="true"/>
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="minEvictableIdleTimeMillis" value="300000"/>
    </bean>

    <!-- 4. 配置动态数据源(核心:继承 AbstractRoutingDataSource) -->
    <bean id="dynamicDataSource" class="com.zhanghe.webconfig.datasource.DynamicDataSource">
        <!-- 目标数据源映射:Key → 数据源实例 -->
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="video" value-ref="videoDataSource"/> <!-- Key=video 对应主数据源 -->
                <entry key="ad" value-ref="adDataSource"/>     <!-- Key=ad 对应特殊数据源 -->
            </map>
        </property>
        <!-- 默认数据源:当 Key 不存在时使用 -->
        <property name="defaultTargetDataSource" ref="videoDataSource"/>
    </bean>

    <!-- 5. 配置 JdbcTemplate(使用动态数据源) -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dynamicDataSource"/>
    </bean>

    <!-- 6. 配置事务管理器(使用动态数据源) -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dynamicDataSource"/>
    </bean>

    <!-- 7. 启用事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!-- 8. 启用 AOP(用于数据源切换切面) -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

3. 步骤 3:实现动态数据源(继承 AbstractRoutingDataSource)

自定义 DynamicDataSource 类,重写 determineCurrentLookupKey() 方法,通过 DataSourceHolder 获取当前线程的数据源 Key。

(1)数据源持有器(DataSourceHolder):线程安全存储 Key

使用 ThreadLocal 存储当前线程的数据源 Key,避免多线程干扰(每个线程独立持有 Key),并在使用后清除(防止内存泄漏):

import org.apache.commons.lang3.StringUtils;

public class DataSourceHolder {
    // ThreadLocal:存储当前线程的数据源 Key
    private static final ThreadLocal<String> CURRENT_DATA_SOURCE = new ThreadLocal<>();
    // 默认数据源 Key(与动态数据源配置一致)
    private static final String DEFAULT_DATA_SOURCE = "video";

    /**
     * 获取当前数据源 Key
     */
    public static String getCurrentDataSourceKey() {
        return CURRENT_DATA_SOURCE.get();
    }

    /**
     * 设置当前数据源 Key
     */
    public static void setCurrentDataSourceKey(String dataSourceKey) {
        if (StringUtils.isNotBlank(dataSourceKey)) {
            CURRENT_DATA_SOURCE.set(dataSourceKey);
        }
    }

    /**
     * 清除当前数据源 Key(必须调用,防止内存泄漏)
     */
    public static void clearCurrentDataSourceKey() {
        CURRENT_DATA_SOURCE.remove();
    }

    /**
     * 获取默认数据源 Key
     */
    public static String getDefaultDataSourceKey() {
        return DEFAULT_DATA_SOURCE;
    }
}
(2)动态数据源类(DynamicDataSource)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);

    /**
     * 核心方法:返回当前数据源 Key,用于路由到对应的数据源
     */
    @Override
    protected Object determineCurrentLookupKey() {
        // 1. 从 ThreadLocal 获取当前 Key
        String currentKey = DataSourceHolder.getCurrentDataSourceKey();
        // 2. 若 Key 为空,使用默认数据源 Key
        if (org.apache.commons.lang3.StringUtils.isBlank(currentKey)) {
            currentKey = DataSourceHolder.getDefaultDataSourceKey();
        }
        // 3. 日志打印(便于排查问题)
        LOGGER.info("线程[{}]当前使用数据源 Key:{}", Thread.currentThread().getName(), currentKey);
        return currentKey;
    }
}

动态切换数据源:基于注解 + AOP

通过自定义注解 @DataSource 标记方法使用的数据源,配合 AOP 切面在方法执行前设置 Key,执行后清除 Key,实现 “无侵入式切换”。

1. 步骤 1:定义数据源注解(@DataSource)

import java.lang.annotation.*;

/**
 * 数据源切换注解:标记方法使用的数据源 Key
 */
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface DataSource {
    // 数据源 Key,默认使用主数据源(video)
    String value() default "video";
}

2. 步骤 2:实现 AOP 切面(数据源切换逻辑)

切面的核心是:

  • @Before:在方法执行前,从 @DataSource 注解中获取 Key,存入 DataSourceHolder
  • @After:在方法执行后,清除 DataSourceHolder 中的 Key(防止内存泄漏);
  • @Order(1):确保切面优先级高于事务切面(切换数据源必须在事务开启前执行,否则事务会绑定默认数据源)。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect // 标记为切面
@Component // 注册为 Spring Bean
@Order(1) // 优先级:1(高于事务切面的默认优先级)
public class DataSourceAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * 切入点:匹配所有标注 @DataSource 注解的方法
     */
    @Pointcut("@annotation(com.zhanghe.webconfig.datasource.DataSource)")
    public void dataSourcePointcut() {}

    /**
     * 前置通知:方法执行前设置数据源 Key
     */
    @Before("dataSourcePointcut()")
    public void beforeSwitchDataSource(JoinPoint joinPoint) {
        // 1. 获取目标方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 2. 获取 @DataSource 注解
        DataSource dataSourceAnnotation = method.getAnnotation(DataSource.class);
        if (dataSourceAnnotation == null) {
            // 若注解为空,使用默认 Key
            DataSourceHolder.setCurrentDataSourceKey(DataSourceHolder.getDefaultDataSourceKey());
            LOGGER.info("线程[{}]未指定数据源,使用默认数据源", Thread.currentThread().getName());
            return;
        }
        // 3. 从注解中获取 Key 并设置
        String dataSourceKey = dataSourceAnnotation.value();
        DataSourceHolder.setCurrentDataSourceKey(dataSourceKey);
        LOGGER.info("线程[{}]切换数据源:{}", Thread.currentThread().getName(), dataSourceKey);
    }

    /**
     * 后置通知:方法执行后清除数据源 Key(防止内存泄漏)
     */
    @After("dataSourcePointcut()")
    public void afterClearDataSource() {
        DataSourceHolder.clearCurrentDataSourceKey();
        LOGGER.info("线程[{}]清除数据源 Key", Thread.currentThread().getName());
    }
}

双数据源使用:事务传播行为注意事项

在使用 @Transactional 注解时,必须注意事务传播行为,否则数据源切换可能失效。核心原因是:
Spring 事务会在方法执行前绑定数据源,若使用默认传播行为 Propagation.REQUIRED(继承父事务),子方法会复用父事务的数据源,导致切换失效。

1. 正确的事务传播行为:Propagation.REQUIRES_NEW

使用 Propagation.REQUIRES_NEW 新建独立事务,确保数据源切换生效(新事务会重新绑定当前数据源 Key 对应的数据源):

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AdService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 操作 ad 库:使用 @DataSource 切换数据源,事务传播行为设为 REQUIRES_NEW
     */
    @DataSource(value = "ad") // 切换到 ad 数据源
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void deleteExpireAdData() {
        // 执行 ad 库的 SQL(JdbcTemplate 会自动使用 ad 数据源)
        String sql = "DELETE FROM ad_info WHERE expire_time < NOW()";
        jdbcTemplate.update(sql);
        LOGGER.info("删除 ad 库过期数据成功");
    }
}

@Service
public class VideoService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private AdService adService;

    /**
     * 操作 video 库(默认数据源),并调用 ad 库方法
     */
    @Transactional(rollbackFor = Exception.class)
    public void processVideoAndAd() {
        // 1. 操作 video 库(默认数据源)
        String videoSql = "UPDATE video_info SET status = 1 WHERE id = ?";
        jdbcTemplate.update(videoSql, 1L);

        // 2. 调用 ad 库方法(新建事务,数据源切换生效)
        adService.deleteExpireAdData();
    }
}

2. 事务传播行为对比(为什么不能用 REQUIRED)

传播行为 作用 对数据源切换的影响
REQUIRED(默认) 继承父事务,无父事务则新建 子方法复用父事务的数据源,切换失效
REQUIRES_NEW 新建独立事务,暂停父事务 新事务绑定当前数据源 Key,切换生效
SUPPORTS 有父事务则加入,无则非事务 若父事务存在,复用父数据源;否则切换生效
MANDATORY 必须在父事务中执行,否则抛出异常 复用父事务数据源,切换失效

常见问题与避坑指南

1. 数据源切换失效?检查 AOP 优先级

若切换失效,首先确认 DataSourceAspect@Order 优先级是否低于事务切面(事务切面默认优先级为 Ordered.LOWEST_PRECEDENCE,即最小优先级)。@Order(1) 可确保数据源切换在事务开启前执行。

2. 线程安全问题?必须清除 ThreadLocal

DataSourceHolderclearCurrentDataSourceKey() 必须在 @After 中调用,否则 ThreadLocal 会持有线程引用,导致内存泄漏(尤其是线程池场景)。

3. 多数据源 Key 不一致?统一 Key 定义

建议用枚举统一管理数据源 Key,避免硬编码错误:

public enum DataSourceKey {
    VIDEO("video"), AD("ad");

    private final String key;

    DataSourceKey(String key) {
        this.key = key;
    }

    public String getKey() {
        return key;
    }
}

// 使用时:@DataSource(value = DataSourceKey.AD.getKey())

4. 连接池参数优化?避免性能问题

  • maxActive:根据并发量设置(如 20~50),避免过大导致数据库连接耗尽;
  • testWhileIdle:开启空闲连接检测,避免使用失效连接;
  • removeAbandoned:开启遗弃连接回收(removeAbandoned=true),避免连接泄漏。

扩展:Spring Boot 双数据源配置(简化版)

若使用 Spring Boot,可通过 application.yml 配置双数据源,配合 @Configuration 类简化配置:

# application.yml
spring:
  datasource:
    # 主数据源(video)
    video:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/video?useUnicode=true&characterEncoding=utf-8
      username: root
      password: 123456
    # 特殊数据源(ad)
    ad:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/ad?useUnicode=true&characterEncoding=utf-8
      username: root
      password: 123456
    # 动态数据源
    dynamic:
      primary: video # 默认数据源
      datasource:
        video:
          type: com.alibaba.druid.pool.DruidDataSource
        ad:
          type: com.alibaba.druid.pool.DruidDataSource

配置类:

@Configuration
public class DynamicDataSourceConfig {
    // 配置 video 数据源
    @Bean("videoDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.video")
    public DataSource videoDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    // 配置 ad 数据源
    @Bean("adDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.ad")
    public DataSource adDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    // 配置动态数据源
    @Bean("dynamicDataSource")
    @Primary // 优先注入
    public DataSource dynamicDataSource(@Qualifier("videoDataSource") DataSource videoDataSource,
                                       @Qualifier("adDataSource") DataSource adDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("video", videoDataSource);
        targetDataSources.put("ad", adDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(videoDataSource);
        return dynamicDataSource;
    }
}

总结

Spring 双数据源的核心是 AbstractRoutingDataSource + ThreadLocal + AOP

  1. AbstractRoutingDataSource 实现数据源路由;
  2. ThreadLocal 保证线程安全的 Key 存储;
  3. 注解 + AOP 实现无侵入式切换;
  4. 事务传播行为 REQUIRES_NEW 确保切换生效