Spring Boot 动态多数据源:核心思路与关键考量

01 引言

上一节我们使用Qoder完成了动态数据源的demo,测试结果也没有让人失望。但是生成的代码会给我们带来什么样的思考,如果是我们自己实现,会不会考虑比Agent更加全面呢?

我们带着阅读、学习的态度了解多数据源的动态切换的核心思路。

02 为什么需要动态多数据源

业务场景驱动技术选型:

  • 读写分离:主库写入,从库读取,提升并发能力
  • 数据隔离:不同业务模块使用独立数据库
  • 分库分表:业务增长后的必然选择
  • 灰度发布:新旧数据库并行,逐步迁移流量

核心诉求:在代码无感知的情况下,根据业务需求自动切换数据源。

主要的解决思路分大概两种:

  • 继承AbstractRoutingDataSource
  • 使用数据库的schema

继承AbstractRoutingDataSource 今天主要学习的技术,简单说一下数据库的schema

数据库的schema的前提是一个账号具有多个数据库的读写权限,通过任意一个数据库连接,来操作所有的数据库。每一个SQL都必须通过tableName.表名的形式。

sql 复制代码
select * from test.user where id=1;
update test.user set name='AI' where id=1

03 实现思路

通过注解的方式,类似Mybaits-Plus的动态数据源dynamic-datasource-spring-boot-starter的注解@DS

3.1 定义数据源注解

我们这里直接使用DataSource

java 复制代码
/**
 * 多数据源切换注解
 * 用于在方法或类级别指定数据源
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    
    /**
     * 数据源名称
     * @return 数据源 key
     */
    String value() default "master";
}

关键点:

  • 支持方法级和类级,方法级优先级更高
  • 默认值避免遗忘配置
  • 运行时保留供 AOP 识别

3.2 ThreadLocal 上下文管理

ThreadLocal是保持同一线程同一数据源的关键。

java 复制代码
public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    
    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }
    
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }
    
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove(); // ⚠️ 必须清理!
    }
}

关键点:

  • ThreadLocal 保证线程隔离,无需加锁
  • 每个请求线程独立维护数据源上下文
  • 必须清理:防止线程池复用导致的数据污染

3.3 动态路由数据源

java 复制代码
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    
    /**
     * 决定当前使用哪个数据源
     * @return 数据源 key
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }
}

AbstractRoutingDataSourcespring-jdbc留给我们的扩展点。

Spring 的巧妙设计

  • 每次获取数据库连接前,自动调用 determineCurrentLookupKey()
  • ThreadLocal 获取当前应使用的数据源 key
  • 自动从目标数据源 Map 中查找并返回对应数据源

3.4 AOP自动切换

使用注解自然少不了AOP的切面编程,我们需要通过@DataSource注解获取运行时数据源的信息。

java 复制代码
@Aspect
@Component
@Order(1) // ⚠️ 关键:必须小于事务切面的 Order
public class DataSourceAspect {
    
    @Pointcut("@annotation(DataSource) || @within(DataSource)")
    public void dataSourcePointcut() {}
    
    @Before("dataSourcePointcut()")
    public void before(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        
        // 优先方法注解,其次类注解
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if (dataSource == null) {
            dataSource = joinPoint.getTarget().getClass().getAnnotation(DataSource.class);
        }
        
        if (dataSource != null) {
            DataSourceContextHolder.setDataSourceKey(dataSource.value());
        }
    }
    
    @After("dataSourcePointcut()")
    public void after(JoinPoint joinPoint) {
        DataSourceContextHolder.clearDataSourceKey();
    }
}

执行流程

  1. 方法执行前 → 解析注解 → 设置数据源 key
  2. 执行业务逻辑 → Spring 路由到对应数据源
  3. 方法执行后 → 清理ThreadLocal → 防止内存泄漏

关键点:

  • 优先方法注解,其次类注解
  • 切换数据源的Order必须小于事务切面的Order,确保切换数据源在事务之前执行。

04 配置绑定

上面是整个数据源动态切换的整体思路,但是数据源怎么配置呢?

4.1 yml 配置

yml 复制代码
spring:
  application:
    name: boot-qoder
  
  datasource:
    # master 数据源配置
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/db_master?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: root
    
    # slave1 数据源配置
    slave1:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/db_slave1?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: root
    
    # slave2 数据源配置
    slave2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/db_slave2?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: root

4.2 配置类

java 复制代码
@Configuration
@MapperScan(basePackages = "com.example.bootqoder.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
public class DataSourceConfig {
    
    /**
     * master 数据源
     * @return 数据源实例
     */
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .type(com.zaxxer.hikari.HikariDataSource.class)
                .build();
    }
    
    /**
     * slave1 数据源
     * @return 数据源实例
     */
    @Bean(name = "slave1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create()
                .type(com.zaxxer.hikari.HikariDataSource.class)
                .build();
    }
    
    /**
     * slave2 数据源
     * @return 数据源实例
     */
    @Bean(name = "slave2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave2")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create()
                .type(com.zaxxer.hikari.HikariDataSource.class)
                .build();
    }
    
    /**
     * 创建动态路由数据源
     * @return 动态数据源
     */
    @Bean(name = "dynamicDataSource")
    @Primary
    public DataSource dynamicDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slave1DataSource") DataSource slave1DataSource,
            @Qualifier("slave2DataSource") DataSource slave2DataSource) {
        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
        
        // 配置多个数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave1", slave1DataSource);
        targetDataSources.put("slave2", slave2DataSource);
        
        // 设置目标数据源
        routingDataSource.setTargetDataSources(targetDataSources);
        
        // 设置默认数据源
        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        
        return routingDataSource;
    }
    
    /**
     * 创建 SqlSessionFactory
     * @param dataSource 数据源
     * @return SqlSessionFactory
     * @throws Exception 异常
     */
    @Bean(name = "sqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        
        // 设置 Mapper XML 文件位置
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
        
        // 设置 MyBatis Plus 配置
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setMapUnderscoreToCamelCase(true);
        sessionFactory.setConfiguration(configuration);
        
        return sessionFactory.getObject();
    }
    
    /**
     * 创建 SqlSessionTemplate
     * @param sqlSessionFactory SqlSessionFactory
     * @return SqlSessionTemplate
     */
    @Bean(name = "sqlSessionTemplate")
    @Primary
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

我们需要单独每一个数据源都要交给Spring管理:

  • masterDataSource
  • slave1DataSource
  • slave2DataSource

然后才能创建动态数据源,统一由dynamicDataSource管理,返回的数据源类型为com.example.bootqoder.datasource.DynamicRoutingDataSource,就是3.3创建的动态路由数据源。

最后需要设置sqlSessionFactory,由于使用了Mybaits Plus。就需要将动态数据源设置到com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean里面去。

05 小结

个人感觉AI生成的代码,比我预想的要好多了。动态切换数据源,我事先没有考虑过在事务之后切换数据源的异常,但是AI考虑到了。

随着AI编程的深入,后面可能我们都不太关注代码该怎么写了,从一个执行者转变成管理者,手搓代码可能真的要变成大家调侃的古法编程了。

相关推荐
sheji34162 小时前
【开题答辩全过程】以 基于springboot的健身预约系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
没有bug.的程序员3 小时前
500个微服务上云全线假死:Spring Boot 3.2 自动配置底层的生死狙击
java·spring boot·微服务·kubernetes·自动配置
夜郎king3 小时前
实战教程:Leaflet+SpringBoot 实现地图任意点位点击查看时间功能
spring boot·webgis 时区生成·java时区可视化
程序员柳4 小时前
智能学生管理系统:Spring Boot3+Vue3 前后端分离开发与 Docker 部署
spring boot·后端·docker
毕设源码-钟学长4 小时前
【开题答辩全过程】以 基于SpringBoot的健康系统为例,包含答辩的问题和答案
java·spring boot·后端
慧都小项4 小时前
Java开发工具MyEclipse发布v2026.1:支持Java25和Spring Boot4、AI功能升级
java·spring boot·myeclipse
ZHANG13HAO4 小时前
校园食堂订餐系统微服务架构设计与实现
spring boot
独断万古他化4 小时前
【抽奖系统开发实战】Spring Boot 项目的用户模块设计:注册登录、权限管控与敏感数据加密
java·spring boot·redis·后端·mvc·jwt·拦截器
Amour恋空4 小时前
SpringBoot使用SpringAi完成简单智能助手
java·spring boot·后端