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

相关推荐
唐叔在学习2 小时前
为了不付费,我硬生生用AI开发了一个跨平台待办应用
后端·程序员·ai编程
好家伙VCC2 小时前
**NumPy中的高效数值计算:从基础到进阶的实战指南**在现代数据科学与机器学习领域
java·python·机器学习·numpy
旷世奇才李先生2 小时前
066基于java的中医养生系统-springboot+vue
java·vue.js·spring boot
武子康2 小时前
大数据-249 离线数仓 - 电商分析 Hive 数仓实战:订单拉链表到 DWS 宽表设计与加载脚本详解
大数据·后端·apache hive
qingy_20462 小时前
Java基础:数据类型
java·开发语言·算法
躲在没风的地方2 小时前
异常执行顺序
java·运维·服务器·spring boot
没有bug.的程序员2 小时前
黑客僵尸网络的降维打击:Spring Cloud Gateway 自定义限流剿杀 Sentinel 内存黑洞
java·网络·spring·gateway·sentinel
予枫的编程笔记2 小时前
【面试专栏|Java并发编程】ConcurrentHashMap并发原理详解:JDK7 vs JDK8 核心对比
java·并发编程·hashmap·java面试·集合框架·jdk8·jdk7
程序员在线炒粉8元1份顺丰包邮送可乐2 小时前
【Java 实现】用友 BIP V5 版本与飞书集成单点登录(飞书免密登录到用友 ERP)
java·开发语言·飞书·用友 bip