在企业级应用开发中,多数据源切换是一个非常常见的需求。无论是为了实现读写分离 (主库写,从库读)、分库分表 ,还是多租户架构,掌握动态数据源切换都是进阶 Java 开发的必备技能。
本教程将带你从零开始,基于 Spring Boot 和 AbstractRoutingDataSource 实现一个轻量级、高性能的动态数据源切换方案。
一、核心原理:AbstractRoutingDataSource
Spring 提供了一个名为 AbstractRoutingDataSource 的抽象类,它是实现动态切换的核心。
工作机制:
- 路由键(Lookup Key): 系统维护一个"地图",Key 是数据源标识(如 "master", "slave"),Value 是具体的 DataSource 对象。
- 动态决策: 每次数据库操作前,Spring 会调用
determineCurrentLookupKey()方法。 - 线程隔离: 我们通常使用
ThreadLocal来存储当前线程应该使用的数据源标识,确保多线程环境下互不干扰。
流程:
业务代码 → AOP切面(设置ThreadLocal) → AbstractRoutingDataSource(获取ThreadLocal值) → 路由到具体DataSource → 执行SQL → AOP切面(清除ThreadLocal)
二、环境准备与依赖
我们将使用 Druid 连接池(阿里巴巴开源,监控功能强大)和 MyBatis-Plus(简化开发)。
Maven 依赖 (pom.xml):
<dependencies>
<!-- Web 基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<!-- MyBatis-Plus (可选,简化Mapper开发) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- AOP 支持 (必须) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
三、配置文件 (application.yml)
我们需要在配置文件中定义多个数据源。
spring:
datasource:
# 主数据源 (Master)
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db_master?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
# 从数据源 (Slave)
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db_slave?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
四、核心代码实现
这是最关键的部分,我们将分步实现。
1、创建数据源枚举
用于规范数据源的标识,避免魔法值。
public enum DataSourceType {
MASTER("master"),
SLAVE("slave");
private final String name;
DataSourceType(String name) { this.name = name; }
public String getName() { return name; }
}
2、实现数据源上下文持有者 (ContextHolder)
使用 ThreadLocal 保证线程安全。
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
// 设置数据源 key
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}
// 获取数据源 key
public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}
// 清除数据源 key (非常重要,防止内存泄漏和线程复用导致的数据污染)
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
}
3、自定义注解
为了让代码更优雅,我们定义一个注解来标记在 Service 方法上。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {
DataSourceType value() default DataSourceType.MASTER;
}
4、实现动态数据源路由类
继承 AbstractRoutingDataSource。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从 ThreadLocal 中获取当前应该使用的数据源标识
return DataSourceContextHolder.getDataSourceKey();
}
}
5、数据源配置类
将所有数据源组装到路由类中。
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary // 默认数据源
public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.getName(), masterDataSource);
targetDataSources.put(DataSourceType.SLAVE.getName(), slaveDataSource);
DynamicDataSource routingDataSource = new DynamicDataSource();
routingDataSource.setTargetDataSources(targetDataSources);
// 默认使用主库Master
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
}
6、AOP 切面实现自动切换
这是"自动化"的关键。我们需要在方法执行前设置数据源,执行后清除。
️ 重要提示: AOP 切面的优先级 (@Order) 必须高于事务管理器 (@Transactional)。因为事务开启后,连接就已经获取了,此时再切换数据源是无效的。
@Aspect
@Component
@Order(1) // 确保 Order 值小于事务的 Order (默认事务是 Ordered.LOWEST_PRECEDENCE)
public class DataSourceAspect {
@Around("@annotation(dynamicDataSource)")
public Object switchDataSource(ProceedingJoinPoint point, DynamicDataSource dynamicDataSource) throws Throwable {
// 1. 获取注解中的值
DataSourceType type = dynamicDataSource.value();
// 2. 设置到 ThreadLocal
DataSourceContextHolder.setDataSourceKey(type.getName());
try {
// 3. 执行目标方法
return point.proceed();
} finally {
// 4. 务必在 finally 块中清除,防止线程池复用导致数据错乱
DataSourceContextHolder.clearDataSourceKey();
}
}
}
五、业务层使用示例
手动切换数据源:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void addUser(User user) {
userMapper.insert(user);
}
}
// 外层调用方
@Service
public class OuterService {
@Autowired
private UserService userService;
public void businessMethod(User user) {
// 先切换数据源
DataSourceContextHolder.master();
try {
// 再调用(此时开启事务)
userService.addUser(user);
} finally {
DataSourceContextHolder.clearDataSourceKey();
}
}
}
动态切换数据源:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 默认走主库(或者不写注解,因为默认是 Master)
@DynamicDataSource(DataSourceType.MASTER)
@Transactional
public void addUser(User user) {
userMapper.insert(user);
}
// 查询操作走从库
@DynamicDataSource(DataSourceType.SLAVE)
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
六、避坑指南与最佳实践
-
事务与切换顺序
这是 90% 的开发者会遇到的坑。如果你使用了
@Transactional,请确保你的数据源切换切面在事务开启之前执行。- 错误做法:先开启事务,再切换数据源(切换无效,仍使用默认库)。
- 正确做法:如上所示,设置
@Order(1)。
-
ThreadLocal 内存泄漏
在使用线程池(如 Tomcat 容器)的环境中,线程会被复用。如果不在
finally块中调用remove()清除 ThreadLocal,下一个请求可能会错误地继承上一个请求的数据源设置。 -
MyBatis-Plus 配置
如果你使用 MyBatis-Plus,确保
SqlSessionFactory注入的是我们定义的dynamicDataSource,而不是具体的masterDataSource。在 Spring Boot 自动配置中,通常注入@Primary的 Bean 即可。 -
连接池隔离
主库和从库应该配置独立的连接池参数。例如,主库可能需要更高的
max-active连接数,而从库可以配置较小的连接数。
七、进阶方案:使用开源插件
如果你觉得手写上述代码太繁琐,或者担心维护成本,强烈推荐使用开源社区成熟的解决方案:dynamic-datasource-spring-boot-starter。
优点:
- 零代码配置,开箱即用。
- 支持 SpEL 表达式(如根据 userId 取模分库)。
- 支持多数据源嵌套调用。
- 官方维护,兼容性好。
使用方式:
- 引入依赖
dynamic-datasource-spring-boot-starter。 - 在
application.yml中配置数据源。 - 直接使用
@DS("slave")注解即可。
通过本教程,你应该已经掌握了 Spring Boot 下多数据源切换的核心原理和实现细节。无论是手写实现还是使用插件,理解底层的 AbstractRoutingDataSource 和 ThreadLocal 机制都是至关重要的。