0%

spring配置双数据源

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

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

双数据源核心原理

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- 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)
1
2
3
4
5
6
7
8
9
# 主数据源(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 配置数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?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),并在使用后清除(防止内存泄漏):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

1
2
3
4
5
6
7
8
9
10
11
12
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):确保切面优先级高于事务切面(切换数据源必须在事务开启前执行,否则事务会绑定默认数据源)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 对应的数据源):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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,避免硬编码错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 类简化配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 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

配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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 确保切换生效

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10