多数据源切换实战:从业务场景到3种实现方案全解析

多数据源切换实战:从业务场景到3种实现方案全解析

在微服务、SaaS、高并发等复杂架构下,单一数据源往往无法满足业务需求。多数据源动态切换已经成为后端开发必备技能。本文将结合真实业务场景,由浅入深地讲解三种主流的实现方案,并通过流程图、类图辅助理解,助你彻底掌握多数据源切换技术。


一、为什么要多数据源?三个典型场景

场景①:SaaS 运营端操作租户应用库

  • 业务描述:SaaS 平台的运营端(库A)为租户手动开通某个应用账号时,需要在租户的应用库(库B)中插入一条用户数据。库A和库B物理隔离,访问库B必须切换数据源。
  • 难点:运营端代码需要动态切换目标库,且租户数量可能成百上千,数据源需支持运行时注册。

场景②:MySQL 读写分离

  • 业务描述:写操作走主库(保证一致性),读操作走从库(分担查询压力)。系统需要根据操作类型自动切换数据源。
  • 难点:如何无侵入地拦截 DAO 方法,识别读写操作并选择对应数据源。

场景③:管理端 + 独立订单库

  • 业务描述:管理端操作管理库(配置、用户等),但订单表因数据量庞大独立为一个订单库。管理端查询订单时需切换到订单库。
  • 难点:同一个 Service 中可能混合调用不同库的 Mapper,数据源切换必须线程安全且精确。

二、多数据源切换的核心原理

所有切换方案的本质都是:在运行时动态选择数据源,并保证同一个线程内后续数据库操作使用选定的数据源
写操作
读操作
租户A
租户B
应用请求
判断目标数据源
主库
从库
租户A库
租户B库
执行SQL
返回结果

Spring 提供的 AbstractRoutingDataSource 是大多数自定义方案的基石。它维护了一个 数据源映射表 (Map<Object, DataSource>),并通过 determineCurrentLookupKey() 方法返回当前线程应该使用的数据源的 key
uses
AbstractRoutingDataSource
-Map<Object, DataSource> targetDataSources
-Object defaultTargetDataSource
+determineCurrentLookupKey() : Object
+getConnection() : Connection
DynamicDataSource
+determineCurrentLookupKey() : Object
DataSourceContextHolder
-ThreadLocal<String> contextHolder
+set(String key)
+get() : String
+clear()

核心流程

  1. 在请求进入时(如 Controller 或 Service 层)确定目标数据源的标识(如 "master", "slave", "tenant_001")。
  2. 将该标识存入 ThreadLocal
  3. 调用 AbstractRoutingDataSource.getConnection() 时,内部调用 determineCurrentLookupKey()ThreadLocal 获取 key,并返回对应数据源的连接。
  4. 操作完成后清理 ThreadLocal(防止线程池复用导致串库)。

下面我们逐一解析三种落地方案。


三、方案一:手动继承 AbstractRoutingDataSource + AOP+注解

这是最灵活、最贴近原理的实现方式,适合对 Spring 源码有掌控力的团队。

3.1 步骤1:定义数据源上下文持有器

java 复制代码
public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }

    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}

3.2 步骤2:自定义动态数据源类

java 复制代码
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

3.3 步骤3:配置数据源(以多租户为例)

java 复制代码
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.ops")
    public DataSource opsDataSource() { ... }

    @Bean
    @ConfigurationProperties("spring.datasource.tenant1")
    public DataSource tenant1DataSource() { ... }

    @Bean
    public DynamicDataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("ops", opsDataSource());
        targetDataSources.put("tenant1", tenant1DataSource());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(opsDataSource()); // 默认运营库
        return dynamicDataSource;
    }
}

3.4 步骤4:用 AOP + 注解实现无侵入切换

定义注解

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value();  // 数据源名称
}

AOP 切面

java 复制代码
@Aspect
@Component
public class DataSourceAspect {
    @Before("@annotation(dataSource)")
    public void switchDataSource(JoinPoint point, DataSource dataSource) {
        DataSourceContextHolder.setDataSource(dataSource.value());
    }

    @After("@annotation(dataSource)")
    public void restoreDataSource(DataSource dataSource) {
        DataSourceContextHolder.clear();
    }
}

使用示例

java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    @DataSource("orderDB")
    public List<Order> queryFromOrderDB() {
        return orderMapper.selectList();
    }
}

3.5 流程图:AOP+注解切换过程

Service DB DynamicDataSource ThreadLocal AOP Controller Client Service DB DynamicDataSource ThreadLocal AOP Controller Client 请求调用 queryFromOrderDB() 拦截 @DataSource 注解 set("orderDB") 执行目标方法 调用 orderMapper get() "orderDB" 获取 orderDB 的连接 返回数据 方法执行完毕 clear() 返回结果

优点 :完全掌控,可扩展(如支持动态注册新租户数据源)。
缺点:需要手写大量配置和切面,易出错。


四、方案二:MyBatis-Plus 动态数据源(推荐)

MyBatis-Plus 官方提供了 dynamic-datasource 模块,开箱即用,支持读写分离、多库切换,是目前中小型项目最常用的方案。

4.1 引入依赖

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>4.3.0</version>
</dependency>

4.2 配置文件(application.yml)

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: master  # 默认数据源
      datasource:
        master:        # 主库
          url: jdbc:mysql://localhost:3306/master_db
          username: root
          password: 123456
        slave_1:       # 从库1
          url: jdbc:mysql://localhost:3306/slave_db
          username: root
          password: 123456
        order_db:      # 独立订单库
          url: jdbc:mysql://localhost:3306/order_db
          username: root
          password: 123456

4.3 使用注解 @DS

java 复制代码
@Service
public class OrderService {

    @DS("order_db")
    public List<Order> getOrders() {
        return orderMapper.selectList();
    }

    @DS("slave_1")
    public List<User> getUsersFromSlave() {
        return userMapper.selectList();
    }

    @DS("master")   // 也可省略,因为 primary = master
    public void updateUser(User user) {
        userMapper.updateById(user);
    }
}

4.4 读写分离自动识别(可选)

配合 dynamic-datasource 的读写分离插件,只需配置从库名称规则:

yaml 复制代码
spring.datasource.dynamic:
  primary: master
  strategy: round  # 负载均衡策略
  datasource:
    master:
      url: ...
    slave_1:
      url: ...
    slave_2:
      url: ...

然后在方法上使用 @DS("slave") 即可自动轮询从库。更高级的用法:配置 mybatis-plus 拦截器,根据 SQL 类型(SELECT / INSERT/UPDATE/DELETE)自动切换,连注解都省了

4.5 原理简析

dynamic-datasource 本质上也是基于 AbstractRoutingDataSource,但封装了:

  • 动态数据源加载与刷新
  • @DS 注解解析(支持类、方法级别)
  • 底层使用 DynamicDataSourceContextHolder 管理 ThreadLocal
  • 配合 Spring 事务时,自动保证同一个事务内使用同一个数据源(避免跨库事务)



业务方法
是否有 @DS 注解?
解析注解值
使用 primary 数据源
存入 DynamicDataSourceContextHolder
执行 SQL
清除 ThreadLocal

优点 :配置简单,注解清爽,支持多租户动态添加数据源,社区活跃。
缺点:对跨数据源事务(如同时操作 master 和 order_db)支持较弱,需配合 Seata 等分布式事务方案。


五、方案三:ShardingSphere-JDBC(适合分库分表+读写分离)

当你的场景不仅仅是多数据源切换,还涉及水平分表、分库、分布式事务时,ShardingSphere-JDBC 是更强大的选择。

5.1 引入依赖

xml 复制代码
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.3.2</version>
</dependency>

5.2 配置读写分离 + 多库

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: master,slave0,orderds
      master:  # 主库
        type: com.zaxxer.hikari.HikariDataSource
        url: jdbc:mysql://localhost:3306/master_db
      slave0:  # 从库
        url: jdbc:mysql://localhost:3306/slave_db
      orderds: # 订单库
        url: jdbc:mysql://localhost:3306/order_db
    rules:
      readwrite-splitting:
        data-sources:
          ms:
            type: Static
            props:
              auto-aware-data-source-name: master
              write-data-source-name: master
              read-data-source-names: slave0
      sharding:  # 如果需要对 order_db 进行分表,可在此配置
        tables:
          t_order:
            actual-data-nodes: orderds.t_order_$->{0..1}
            table-strategy:
              inline:
                sharding-column: order_id
                algorithm-expression: t_order_$->{order_id % 2}
    props:
      sql-show: true

5.3 代码中零感知切换

ShardingSphere 会拦截所有 SQL,自动根据规则路由到对应的真实数据源或表。对于读写分离,它会自动识别 SELECT 语句走从库,INSERT/UPDATE/DELETE 走主库。

java 复制代码
@Service
public class OrderService {
    // 无需任何注解,直写标准 MyBatis-plus Mapper 即可
    public void createOrder(Order order) {
        orderMapper.insert(order);  // 自动走主库
    }

    public List<Order> listOrders() {
        return orderMapper.selectList(); // 自动走从库(轮询)
    }

    public Order getFromShardingTable(Long orderId) {
        // 根据 orderId 路由到 orderds.t_order_0 或 t_order_1
        return orderMapper.selectById(orderId);
    }
}

5.4 架构图

SQL
解析
路由规则


分片键
应用
ShardingSphere JDBC
逻辑SQL
路由
主库
主从复制
订单库分表

优点 :功能最强,统一处理分库分表、读写分离、分布式事务,对业务代码几乎无侵入。
缺点:配置复杂,性能有一定损耗(SQL 解析与路由),学习成本高。


六、三种方案对比总结

特性 方案一:手动继承 + AOP 方案二:MyBatis-Plus 动态数据源 方案三:ShardingSphere-JDBC
实现复杂度 高(需手写大量基础设施) 低(依赖 starter,几乎零配置) 中高(需了解分片规则)
功能范围 仅多数据源切换 多数据源 + 读写分离 + 动态加载 分库分表 + 读写分离 + 分布式事务
对业务代码侵入 中(需要加注解或手动 set) 低(@DS 注解) 极低(完全透明)
支持动态增加数据源 需自行实现 支持(通过 API 动态添加) 支持(通过配置中心动态刷新)
跨库事务 不支持(需借助 JTA 或柔性事务) 不支持(建议避免) 支持(集成 Seata)
适用场景 学习原理、简单多库切换(<5个库) 80% 的中小型项目(SaaS、读写分离) 大数据量、分库分表、复杂路由策略
推荐指数 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐

七、最佳实践与避坑指南

  1. 事务与数据源切换的冲突

    Spring @Transactional 开启事务时,会提前绑定连接(数据源)。此时即便你在方法内切换数据源,也不会生效。解决方案

    • 避免在事务方法内切换数据源;
    • 或者使用声明式事务 + @DS 放在事务方法上(MyBatis-Plus 支持);
    • 或者使用编程式事务(TransactionTemplate)。
  2. ThreadLocal 内存泄漏

    务必在请求结束后(如 AOP @After 或拦截器 afterCompletion)中调用 clear(),否则在线程池复用时会串库。

  3. 动态添加数据源

    如果使用方案二,可以调用 DynamicRoutingDataSource.addDataSource(key, dataSource) 实现运行时添加数据源,适合多租户场景。

  4. 读写分离主从延迟

    对于实时性要求高的读操作(如刚写完就查),可通过强制走主库:@DS("master") 或使用HintManager


八、总结

多数据源切换是大型项目绕不开的课题。根据业务复杂度选择方案:

  • 入门/学习 :手写AbstractRoutingDataSource理解原理;
  • 快速开发/SaaS/读写分离:MyBatis-Plus 动态数据源,性价比最高;
  • 超大规模/分库分表:ShardingSphere-JDBC,一劳永逸。

🧩 思考题:如果系统中已经使用了 ShardingSphere,还想动态添加租户库(非分片库),该如何设计?欢迎留言讨论。

相关推荐
Java小生不才2 小时前
Spring AI文生音
java·人工智能·spring
凯尔萨厮2 小时前
Springboot2.x+Thymeleaf项目创建
java
fish_xk2 小时前
map和set
java·开发语言
李崧正2 小时前
Java技术分享:Lambda表达式与函数式编程
java·开发语言·python
老了,不知天命2 小时前
鳶尾花項目JAVA
java·开发语言·机器学习
二哈赛车手2 小时前
新人笔记---实现简易版的rag的bm25检索(利用ES),以及RAG上传时的ES与向量数据库双写
java·数据库·笔记·spring·elasticsearch·ai
winner88812 小时前
从零吃透C++命名空间、std、#include、string、vector
java·开发语言·c++
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题】【Java基础篇】第26题:Java的抽象类和接口有哪些区别
java·开发语言·面试
AIMath~3 小时前
雪花算法+ZooKeeper解决方案+RPC是什么
分布式·zookeeper·云原生