楼主: Yqw1230
25 0

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

  • 0关注
  • 0粉丝

等待验证会员

学前班

40%

还不是VIP/贵宾

-

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

楼主
Yqw1230 发表于 昨天 17:47 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

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

经管之家联合CDA

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

感谢您参与论坛问题回答

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

+2 论坛币

为什么现代项目离不开多数据源?

在早期的Java开发中,大多数系统采用“单数据库”架构,所有业务表都存放在同一个MySQL实例中,配置也极为简单,只需设置一个spring.datasource.url即可。然而,随着业务规模扩大和系统复杂度提升,这种模式逐渐暴露出诸多问题。 以电商系统为例,用户表每日新增记录可达百万级,订单表更是积累到千万级别。若将两者置于同一数据库,一次简单的订单查询可能因全表扫描导致响应时间长达数秒,严重影响用户体验。此外,在政务或金融类项目中,出于合规要求,敏感信息(如身份证号、手机号)必须存储在独立的加密数据库中,而普通业务数据则存放于常规库,实现物理隔离。再比如跨部门报表场景:财务数据位于财务专用库,运营数据分布在运营库中,统计分析时必须同时访问多个数据源进行汇总。 由此可见,多数据源已不再是可选项,而是应对高并发、保障数据安全、满足业务解耦的刚性需求。

现成框架为何常踩坑?两大痛点揭秘

虽然市面上存在不少多数据源解决方案(如Mybatis-plus的多数据源Starter),但在实际使用中却频繁出现问题,主要原因集中在以下两点: 1. 版本兼容性差
许多第三方Starter对SpringBoot或Mybatis的版本有严格限制。例如某Starter要求最低SpringBoot 2.5.x,而你的项目仍运行在2.2.x甚至更早版本。一旦强行升级,可能引发依赖冲突,牵一发而动全身,风险极高。 2. 扩展能力受限
多数框架仅支持基于包路径或注解的方式进行数据源切换,灵活性不足。当遇到需要根据“用户角色”动态选择数据源的场景——例如管理员访问完整数据库,普通用户只能读取脱敏后的副本库——这些框架便无能为力,最终仍需深入底层自定义实现。 因此,与其依赖封装过重、适应性弱的框架,不如掌握手动实现方式。虽然代码量略有增加,但具备更高的稳定性、灵活性,并且兼容任意版本的SpringBoot。

核心机制解析:AbstractRoutingDataSource 的工作原理

Spring 提供的AbstractRoutingDataSource是实现动态数据源切换的关键组件。它本质上是一个“数据源路由器”,其运作逻辑如下: 该类内部维护一个Map<Object, DataSource>,用于注册多个真实的数据源实例。例如:
  • key = "userDB" → 用户库数据源
  • key = "orderDB" → 订单库数据源
每当执行SQL操作时,Spring会自动调用其determineCurrentLookupKey()方法,获取当前线程应使用的数据源标识(即key),然后从Map中查找对应的数据源并执行操作。 要使其正常工作,开发者只需完成两个关键步骤:
  1. 将多个数据源注入到AbstractRoutingDataSource的映射表中;
  2. 重写determineCurrentLookupKey()方法,返回当前上下文所需的数据源key。
为了保证线程安全,通常借助ThreadLocal来保存当前线程的数据源key。这样不同线程可以独立持有各自的key值,互不干扰。例如:
  • 线程A 设置 key 为 "userDB",访问用户库;
  • 线程B 设置 key 为 "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中定义两个独立的数据源配置,注意避免使用默认前缀spring.datasource,防止与SpringBoot自动配置产生冲突。推荐使用自定义命名空间,如datasource.userdatasource.order。 接着编写Java配置类,分别创建用户库和订单库的DataSource Bean,并将其注册进Spring容器。同时,务必排除SpringBoot默认的数据源自动装配类(如DataSourceAutoConfiguration),以免造成Bean冲突。
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;
    }
}
通过上述配置,我们完成了多数据源的基础准备,接下来便可构建路由逻辑与切换工具。

这里需要注意: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)

虽然可以直接调用方法来切换数据源,但若在每个业务逻辑中都手动处理,代码会显得冗余且难以维护。为此,我们可以借助“自定义注解 + AOP 切面”的方式实现自动切换。

首先,定义一个名为 @DataSource 的注解,用于标记需要指定数据源的方法:

import java.lang.annotation.*;

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

然后,编写 AOP 切面类,拦截所有添加了 @DataSource 注解的方法,在方法执行前自动切换数据源,并在执行完毕后清除 ThreadLocal 中的 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 层的方法上添加 @DataSource 注解,观察是否能正确路由到目标数据库。

例如,在 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 存储的内容,当线程被线程池复用时,可能会携带上一次请求的数据源 key,导致下一次操作误用错误的数据源。因此,在 AOP 的环绕通知中,必须在 finally 块中调用 clearDataSourceKey(),确保无论方法是否抛出异常,都能及时清理上下文。

2. 注意事务与数据源切换的执行顺序
当方法同时标注了 @Transactional@DataSource 时,由于事务切面默认优先级较高,可能在数据源切换之前就已开启事务,导致仍使用默认数据源。解决方案有两种:一是将 @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 04:31