Spring Boot 2.0动态多数据源切换实战教程

在企业级应用开发中,多数据源切换是一个非常常见的需求。无论是为了实现读写分离 (主库写,从库读)、分库分表 ,还是多租户架构,掌握动态数据源切换都是进阶 Java 开发的必备技能。

本教程将带你从零开始,基于 Spring BootAbstractRoutingDataSource 实现一个轻量级、高性能的动态数据源切换方案。

一、核心原理:AbstractRoutingDataSource

Spring 提供了一个名为 AbstractRoutingDataSource 的抽象类,它是实现动态切换的核心。

工作机制:

  1. 路由键(Lookup Key): 系统维护一个"地图",Key 是数据源标识(如 "master", "slave"),Value 是具体的 DataSource 对象。
  2. 动态决策: 每次数据库操作前,Spring 会调用 determineCurrentLookupKey() 方法。
  3. 线程隔离: 我们通常使用 ThreadLocal 来存储当前线程应该使用的数据源标识,确保多线程环境下互不干扰。

流程:
业务代码AOP切面(设置ThreadLocal)AbstractRoutingDataSource(获取ThreadLocal值)路由到具体DataSource执行SQLAOP切面(清除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);
    }
}
六、避坑指南与最佳实践
  1. 事务与切换顺序

    这是 90% 的开发者会遇到的坑。如果你使用了 @Transactional,请确保你的数据源切换切面在事务开启之前执行。

    • 错误做法:先开启事务,再切换数据源(切换无效,仍使用默认库)。
    • 正确做法:如上所示,设置 @Order(1)
  2. ThreadLocal 内存泄漏

    在使用线程池(如 Tomcat 容器)的环境中,线程会被复用。如果不在 finally 块中调用 remove() 清除 ThreadLocal,下一个请求可能会错误地继承上一个请求的数据源设置。

  3. MyBatis-Plus 配置

    如果你使用 MyBatis-Plus,确保 SqlSessionFactory 注入的是我们定义的 dynamicDataSource,而不是具体的 masterDataSource。在 Spring Boot 自动配置中,通常注入 @Primary 的 Bean 即可。

  4. 连接池隔离

    主库和从库应该配置独立的连接池参数。例如,主库可能需要更高的 max-active 连接数,而从库可以配置较小的连接数。

七、进阶方案:使用开源插件

如果你觉得手写上述代码太繁琐,或者担心维护成本,强烈推荐使用开源社区成熟的解决方案:dynamic-datasource-spring-boot-starter

优点:

  • 零代码配置,开箱即用。
  • 支持 SpEL 表达式(如根据 userId 取模分库)。
  • 支持多数据源嵌套调用。
  • 官方维护,兼容性好。

使用方式:

  1. 引入依赖 dynamic-datasource-spring-boot-starter
  2. application.yml 中配置数据源。
  3. 直接使用 @DS("slave") 注解即可。

通过本教程,你应该已经掌握了 Spring Boot 下多数据源切换的核心原理和实现细节。无论是手写实现还是使用插件,理解底层的 AbstractRoutingDataSourceThreadLocal 机制都是至关重要的。

相关推荐
语戚2 小时前
力扣 2463. 最小移动总距离 —— 动态规划 & 贪心排序全解(Java 实现)
java·算法·leetcode·贪心算法·动态规划·力扣·dp
IT_陈寒2 小时前
Vue这个响应式陷阱让我加了两天班
前端·人工智能·后端
techdashen2 小时前
Go 1.25 新特性:Flight Recorder —— 像黑匣子一样捕捉线上 Bug
java·golang·bug
妃衣2 小时前
Html转word追加篇,关于hr标签分割线的显示
java·html·word
A_QXBlms2 小时前
企微群发消息技术实现:定时任务+模板消息
java·mybatis·企业微信
武子康2 小时前
大数据-268 实时数仓-ODS 层 Flink+Kafka+HBase实时流处理:Kafka数据写入维度表实战
大数据·后端·flink
小李子呢02112 小时前
前端八股---axios封装
java·前端·javascript
斌味代码2 小时前
SpringBoot 实战总结:踩坑与解决方案全记录
java·spring boot·后端
摇滚侠2 小时前
Groovy 中如何定义集合
java·开发语言·python