Spring Boot 动态多数据源核心思路与关键介绍

 更新时间:2026年03月17日 10:16:27   作者:SimonKing  
这篇文章给大家介绍了Spring Boot 动态多数据源核心思路与关键介绍,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

01 引言

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

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

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

业务场景驱动技术选型:

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

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

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

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

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

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

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

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

关键点:

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

3.2 ThreadLocal 上下文管理

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

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 动态路由数据源

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

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

Spring 的巧妙设计

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

3.4AOP自动切换

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

@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 配置

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 配置类

@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编程的深入,后面可能我们都不太关注代码该怎么写了,从一个执行者转变成管理者,手搓代码可能真的要变成大家调侃的古法编程了。

到此这篇关于Spring Boot 动态多数据源核心思路与关键介绍的文章就介绍到这了,更多相关Spring Boot 动态多数据源内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解Spring AOP的实现方式

    详解Spring AOP的实现方式

    AOP是一种思想,是对某一类事情的集中处理,切面就是指某一类特定的问题,所以AOP可以理解为面向特定方法编程,这篇文章主要介绍了Spring AOP的实现方式,需要的朋友可以参考下
    2024-02-02
  • Java字节与字符流永久存储json数据

    Java字节与字符流永久存储json数据

    本篇文章给大家详细讲述了Java字节与字符流永久存储json数据的方法,以及代码分享,有兴趣的参考学习下。
    2018-02-02
  • 基于Springboot+Mybatis对数据访问层进行单元测试的方式分享

    基于Springboot+Mybatis对数据访问层进行单元测试的方式分享

    本文将介绍一种快高效、可复用的解决测试方案——对数据访问层做单元测试,文章通过代码示例介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2023-07-07
  • Maven是什么?Maven的概念+作用+仓库的介绍+常用命令的详解

    Maven是什么?Maven的概念+作用+仓库的介绍+常用命令的详解

    Maven是一个项目管理工具,它包含了一个对象模型。一组标准集合,一个依赖管理系统。和用来运行定义在生命周期阶段中插件目标和逻辑.,本文给大家介绍Maven的概念+作用+仓库的介绍+常用命令,感兴趣的的朋友跟随小编一起看看吧
    2020-09-09
  • 关于Java中String创建的字符串对象内存分配测试问题

    关于Java中String创建的字符串对象内存分配测试问题

    这篇文章主要介绍了Java中String创建的字符串对象内存分配测试,给大家详细介绍了在创建String对象的两种常用方法比较,通过示例代码给大家介绍的非常详细,需要的朋友可以参考下
    2021-07-07
  • Java基础学习之实参和形参

    Java基础学习之实参和形参

    这篇文章主要介绍了Java基础学习之实参形参,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有一定的帮助,需要的朋友可以参考下
    2021-05-05
  • java实现获取安卓设备里已安装的软件包

    java实现获取安卓设备里已安装的软件包

    本文给大家介绍的是如何获取设备中已经安装的应用软件包的代码,其核心方法原理很简单,我们通过Android中提供的PackageManager类,来获取手机中安装的应用程序信息
    2015-10-10
  • Trace 在多线程异步体系下传递流程解析

    Trace 在多线程异步体系下传递流程解析

    这篇文章主要为大家介绍了Trace 在多线程异步体系下传递流程解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • git stash 和unstash的使用操作,git unstash failed

    git stash 和unstash的使用操作,git unstash failed

    这篇文章主要介绍了git stash 和unstash的使用操作,git unstash failed,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • Java错误org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.sjks.mapper.Use

    Java错误org.apache.ibatis.binding.BindingException: Inval

    本文主要介绍了Java错误org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.sjks.mapper.Use,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06

最新评论