楼主: 杜兰娜
15 0

[作业] SpringBoot动态数据源切换实战 [推广有奖]

  • 0关注
  • 0粉丝

准贵宾(月)

学前班

40%

还不是VIP/贵宾

-

威望
0
论坛币
959 个
通用积分
0
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
20 点
帖子
1
精华
0
在线时间
0 小时
注册时间
2018-1-1
最后登录
2018-1-1

楼主
杜兰娜 发表于 昨天 17:32 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币

作为 Java 开发者,你是否也曾面临这样的困境:项目进行到一半,产品经理突然提出“需要新增一个功能,必须从用户库和订单库同时获取数据”。你满怀信心地查阅 SpringBoot 官方文档,尝试集成 Mybatis-plus 的多数据源 starter,结果启动时却报出“数据源 bean 冲突”的错误。进一步排查发现,项目当前使用的是 SpringBoot 2.2.x 版本,而该 starter 要求最低为 2.5.x,升级又担心影响其他依赖模块,最终折腾到深夜仍无进展?

实际上,这类问题并不少见。我此前协助同事排查线上故障时就遇到过类似案例:他们采用了某第三方多数据源框架,在高并发场景下出现了“数据源切换失效”的严重 bug,导致订单信息误查成用户数据,险些引发生产事故。事后分析发现,该框架底层基于 AOP 切面实现数据源切换,但在并发环境下 ThreadLocal 中的上下文容易丢失,稳定性不足。相比之下,手动继承 AbstractRoutingDataSource 的方式反而更加可靠。本文将完整拆解如何在 SpringBoot 中手动实现动态数据源切换,从原理剖析到代码落地,全程清晰易懂,新手也能轻松上手。

为何多数据源需求日益普遍?

回顾早期开发模式,大多数系统采用“单数据库架构”,所有表集中在同一个 MySQL 实例中,配置也极为简单,只需设置一条 spring.datasource.url 即可。然而随着业务规模扩大,“单库”模式逐渐暴露出诸多瓶颈:

  • 例如在电商平台中,用户表每日新增记录可达百万级,订单表更是达到千万级别。若两者共用一个数据库,查询一次订单列表可能耗时超过三秒,严重影响用户体验;
  • 政务类系统出于合规要求,敏感信息(如身份证号、手机号)需存储于独立加密数据库,普通业务数据则存放于常规库中;
  • 跨部门协作场景下,财务数据位于财务专用库,运营数据存于运营库,报表统计时必须跨库汇总。

由此可见,多数据源已不再是“是否要引入”的选择题,而是实际业务驱动下的刚性需求。

为何现成框架常踩坑?两大痛点解析

尽管市面上存在多种多数据源解决方案,但在实际应用中仍频繁出现问题,主要原因有两点:

  1. 版本兼容性差:许多第三方 starter 对 SpringBoot 或 MyBatis 的版本有严格限制。例如你的项目基于 SpringBoot 2.1.x 构建,则无法使用 Mybatis-plus 3.5.x 提供的多数据源组件,强行升级可能导致依赖冲突,维护成本极高;
  2. 扩展能力有限:多数框架仅支持按包路径或注解方式进行数据源路由,缺乏灵活性。当业务逻辑需要根据“用户角色”动态选择数据源(如管理员访问完整数据库,普通用户只能读取脱敏库),这些框架往往无法满足,最终仍需自行修改底层逻辑。

因此,与其盲目依赖“开箱即用”的框架,不如掌握手动实现方式。虽然代码量略有增加,但具备更高的稳定性、灵活性,并能适配任意版本的 SpringBoot 环境。接下来我们逐步讲解具体实现步骤。

核心机制:AbstractRoutingDataSource 的作用原理

实现动态数据源切换的关键在于 Spring 提供的 AbstractRoutingDataSource 类。理解其工作机制是成功编码的前提。

该类本质上是一个“数据源路由器”,内部维护一个 Map 结构,用于存储多个真实数据源实例。例如 key 为 “userDB” 时对应用户数据库连接池,key 为 “orderDB” 则指向订单库连接池。当执行 SQL 操作时,系统会自动调用其 determineCurrentLookupKey() 方法,获取当前线程应使用的数据源标识(key),再通过此 key 从 Map 中取出对应的数据源来执行操作。

整个过程由 Spring 自动调度完成,开发者只需完成两个关键动作:

  • 将多个数据源注册进 AbstractRoutingDataSource 的映射表中;
  • 重写 determineCurrentLookupKey() 方法,明确返回当前所需的数据源 key。

其中,确定当前 key 的常用做法是借助 ThreadLocal 存储线程私有的数据源标识,确保多线程环境下各线程之间的数据源选择互不干扰。例如线程 A 设置 key 为 “userDB”,线程 B 设置为 “orderDB”,彼此独立运行,不会发生混淆。

# 自定义多数据源配置
datasource:
  user: # 用户库
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  order: # 订单库
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456

# Mybatis配置(正常配就行,不用改)
mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.demo.entity

三步实现动态数据源切换

第一步:配置多数据源(YAML + 配置类)

首先,在 application.yaml 文件中定义两个独立的数据源配置,分别对应用户库与订单库。注意使用自定义前缀(如 datasource.user 和 datasource.order),避免与默认的 spring.datasource 冲突。

import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

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

// 排除默认数据源自动配置
@Configuration
@Import(DataSourceAutoConfiguration.class)
public class DynamicDataSourceConfig {

    // 注入用户库数据源(前缀对应yaml里的datasource.user)
    @Bean(name = "userDataSource")
    @ConfigurationProperties(prefix = "datasource.user")
    public DataSource userDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 注入订单库数据源(前缀对应yaml里的datasource.order)
    @Bean(name = "orderDataSource")
    @ConfigurationProperties(prefix = "datasource.order")
    public DataSource orderDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 配置动态数据源(把两个数据源装进Map)
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        
        // 1. 设置默认数据源(当没指定key时,用这个)
        dynamicDataSource.setDefaultTargetDataSource(userDataSource());
        
        // 2. 把多个数据源装进Map
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("userDB", userDataSource());
        dataSourceMap.put("orderDB", orderDataSource());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        
        return dynamicDataSource;
    }
}

随后编写 Java 配置类,将上述两个数据源实例注入 Spring 容器。特别需要注意的是,必须排除 SpringBoot 默认的数据源自动装配机制,防止与手动配置产生 Bean 冲突。

在实现动态数据源切换的过程中,首先需要明确的是:DynamicRoutingDataSource 是我们即将创建的“数据源路由器”类,它将继承自 AbstractRoutingDataSource。目前只需先定义好该类名称,后续步骤中会逐步完成其实现。

第二步:构建数据源路由机制(DynamicRoutingDataSource)

创建一个名为 DynamicRoutingDataSource 的新类,并使其继承 AbstractRoutingDataSource,同时重写 determineCurrentLookupKey() 方法。此方法的核心作用是向 Spring 框架返回当前应使用的数据源 key,从而决定实际连接哪一个数据库。

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

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    // 重写方法:返回当前要使用的数据源key
    @Override
    protected Object determineCurrentLookupKey() {
        // 从ThreadLocal里获取当前线程的数据源key
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}

接下来,编写 DynamicDataSourceContextHolder 类,利用 ThreadLocal 来存储当前线程所绑定的数据源 key,相当于为每个线程提供一个独立的“临时上下文容器”。

public class DynamicDataSourceContextHolder {

    // ThreadLocal:存当前线程的数据源key(线程隔离)
    private static final ThreadLocal<String> CURRENT_DATA_SOURCE = new ThreadLocal<>();

    // 设置数据源key
    public static void setDataSourceKey(String dataSourceKey) {
        CURRENT_DATA_SOURCE.set(dataSourceKey);
    }

    // 获取数据源key
    public static String getDataSourceKey() {
        return CURRENT_DATA_SOURCE.get();
    }

    // 清除数据源key(避免内存泄漏)
    public static void clearDataSourceKey() {
        CURRENT_DATA_SOURCE.remove();
    }
}

这一步尤为关键:

DynamicDataSourceContextHolder 中的 set 和 get 方法是实现数据源动态切换的核心逻辑。当需要访问特定数据库时,只需调用 setDataSourceKey("userDB") 或 setDataSourceKey("orderDB"),Spring 就会通过 DynamicRoutingDataSource 自动定位到对应的数据源实例。

第三步:封装数据源切换工具 —— 基于注解与 AOP

虽然可以直接调用 setDataSourceKey 进行数据源切换,但若每次都需要手动编码则显得繁琐且易出错。为此,我们可以引入“注解 + AOP”的方式来自动化这一过程。

设想在 Service 方法上添加 @DataSource(key = "userDB") 注解,AOP 切面便会自动完成数据源切换,并在方法执行完毕后清除 ThreadLocal 中的 key,防止内存泄漏。

首先,定义 @DataSource 注解类型:

import java.lang.annotation.*;

// 注解作用在方法上
@Target(ElementType.METHOD)
// 注解在运行时生效
@Retention(RetentionPolicy.RUNTIME)
// 注解信息会被包含在JavaDoc中
@Documented
public @interface DataSource {
    // 数据源key(默认是userDB)
    String key() default "userDB";
}

然后,编写 AOP 切面类,用于拦截所有标注了 @DataSource 的方法,实现“前置设置数据源 + 后置清理 key”的逻辑控制:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class DataSourceAspect {

    // 切入点:拦截所有加了@DataSource注解的方法
    @Pointcut("@annotation(com.example.demo.annotation.DataSource)")
    public void dataSourcePointCut() {}

    // 环绕通知:在方法执行前后做处理
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            // 1. 方法执行前:获取注解里的key,切换数据源
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            DataSource dataSourceAnnotation = method.getAnnotation(DataSource.class);
            if (dataSourceAnnotation != null) {
                String dataSourceKey = dataSourceAnnotation.key();
                DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey);
            }

            // 2. 执行目标方法(比如Service里的查询方法)
            return joinPoint.proceed();
        } finally {
            // 3. 方法执行后:清除数据源key(必须在finally里,避免异常时没清除)
            DynamicDataSourceContextHolder.clearDataSourceKey();
        }
    }
}

最后,别忘了在 Spring Boot 启动类上添加 @EnableAspectJAutoProxy 注解,以启用对 AspectJ AOP 的支持:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // 开启AOP
public class DynamicDataSourceDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DynamicDataSourceDemoApplication.class, args);
    }
}

功能验证:测试动态数据源切换效果

至此,整个动态数据源切换架构已搭建完成。我们可以通过在 Service 层方法上添加注解来进行测试。

例如,在 UserService 中查询用户信息的方法上添加 @DataSource(key = "userDB"):

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    // 查用户库:指定数据源key为userDB
    @DataSource(key = "userDB")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

在 OrderService 中查询订单信息的方法上添加 @DataSource(key = "orderDB"):

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    // 查订单库:指定数据源key为orderDB
    @DataSource(key = "orderDB")
    public Order getOrderById(Long id) {
        return orderMapper.selectById(id);
    }
}

启动应用并调用上述两个服务方法,可以观察到:getUserById 方法自动连接 user_db 数据库,而 getOrderById 则使用 order_db 数据库,数据源切换准确无误。该方案兼容 Spring Boot 2.x 与 3.x 版本,无需因版本差异而调整实现逻辑。

避坑指南:生产环境必须注意的三个细节

尽管整体流程可行,但在实际部署中若忽略以下几点,仍可能导致严重问题。以下是我在项目实践中总结的关键注意事项,请务必重视:

  1. 必须及时清除 ThreadLocal 中的 key

    ThreadLocal 若未被显式清除,在使用线程池的情况下,线程会被复用,导致后续请求可能沿用前一次的 data source key,引发数据错乱。因此,在 AOP 的 around 通知中,必须在 finally 块中调用 clearDataSourceKey(),确保无论方法是否抛出异常,都能安全释放上下文资源。

  2. 事务与数据源切换的执行顺序问题

    若目标方法上同时存在 @Transactional 注解,事务的初始化会在 AOP 之前完成。这意味着如果数据源切换发生在事务开启之后,则事务仍将基于默认数据源进行操作。解决方案有两种:一是将 @DataSource 注解置于事务方法的外层调用方法上;二是调整 AOP 切面的优先级,确保数据源切换切面早于事务切面执行。

  3. 多数据源环境下的连接池配置

    前述代码示例未包含连接池配置,但在生产环境中必须为各数据源配置独立的连接池(如 HikariCP,Spring Boot 默认连接池)。需在 application.yml 等配置文件中为每个数据源设置合理的连接池参数,以保障性能与稳定性。

    datasource:
      user:
        hikari:
          maximum-pool-size: 10 # 最大连接数
          minimum-idle: 5 # 最小空闲连接
          idle-timeout: 300000 # 空闲连接超时时间(5分钟)
      order:
        hikari:
          maximum-pool-size: 10
          minimum-idle: 5
          idle-timeout: 300000

结语:动手实践才是掌握的关键

本文介绍的手动实现 Spring Boot 动态数据源切换方案,已在多个项目中成功落地,涵盖单体服务与微服务架构,其稳定性和可维护性远超部分第三方框架。技术选型的本质不在于复杂度,而在于是否贴合业务需求、易于长期维护。

如果你初次阅读感到理解困难,不必担心 —— 可先将代码导入本地项目,修改数据库地址和包路径后运行一遍,实践过程中自然会加深理解。若遇到“切换失效”或“事务冲突”等问题,欢迎留言交流,共同排查解决。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

关键词:Spring Pring RING boot ING
相关内容:SpringBoot数据源切换

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
jg-xs1
拉您进交流群
GMT+8, 2025-12-6 05:12