Dynamic-datasource多数据源框架深度实践指南

多数据源框架深度解析指南,从原理到实践,从基础到进阶,一站式掌握Dynamic-datasource核心能力。


一、引言:为什么需要多数据源框架?

1.1 无多数据源框架时的痛点

在企业级应用开发中,多数据源场景日益普遍。在没有成熟框架支撑的情况下,开发者往往面临诸多困境:

代码耦合严重:数据源配置硬编码在代码中,切换需修改源码重新部署,难以独立演进。

手动切换繁琐:每次操作需手动获取Connection,代码冗余且易出错,忘记切换会导致数据写入错误库表。

事务管理复杂:跨数据源事务需手动管理,Spring的@Transactional注解失效,需自行处理事务边界和回滚逻辑。

跨数据源事务需要手动管理,Spring的@Transactional注解在多数据源场景下失效,开发者需要自行处理事务边界、回滚逻辑,代码复杂度急剧上升。

可维护性困境

数据源切换逻辑散落在各处,新人难以理解;配置变更需要改动多处代码;测试困难,难以Mock数据源。

1.2 典型业务场景全景

多数据源框架在以下场景中具有不可替代的价值:

场景类型 场景描述 数据源特点
读写分离 主库写入,从库读取,提升系统读性能 一主多从,读负载均衡
多租户架构 每个租户独立数据库,数据物理隔离 动态数据源,按租户切换
系统整合/迁移 新旧系统并行,逐步迁移数据 多业务库,跨库查询
跨业务线交互 不同业务线独立数据库,需数据互通 多业务库,按需切换
分库分表 数据量大,单库性能瓶颈 多分片库,路由规则

二、方案对比:多数据源解决方案选型

2.1 Spring原生AbstractRoutingDataSource

Spring提供了AbstractRoutingDataSource抽象类,是实现多数据源的基础方案。内部维护数据源Map,通过determineCurrentLookupKey()决定使用哪个数据源。

核心实现要点:需自行实现DataSourceContextHolder(ThreadLocal存储)、DynamicDataSource(路由决策)、自定义注解、AOP切面等多个组件。

优缺点分析

优点 缺点
无需引入第三方依赖 需要自行实现注解、切面、上下文管理
完全可控,灵活性高 功能单一,缺乏动态数据源管理能力
学习成本低 无连接池监控、无负载均衡等高级特性
Spring原生支持 事务管理需自行处理

2.2 ShardingSphere

ShardingSphere是Apache顶级项目,提供数据分片、读写分离等分布式数据库解决方案。

核心能力

  • 数据分片:支持分库分表,提供多种分片策略
  • 读写分离:支持一主多从,自动路由
  • 弹性伸缩:支持数据迁移、扩缩容
  • 数据加密:敏感数据自动加密解密

配置示例

spring:
  shardingsphere:
    datasource:
      names: ds-master,ds-slave
      ds-master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/master_db
        username: root
        password: root
      ds-slave:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/slave_db
        username: root
        password: root
    rules:
      readwrite-splitting:
        data-sources:
          myds:
            write-data-source-name: ds-master
            read-data-source-names:
              - ds-slave
            load-balancer-name: round_robin
        load-balancers:
          round_robin:
            type: ROUND_ROBIN

适用场景与局限性

适用场景 局限性
大规模数据分片 配置复杂,学习成本高
复杂的读写分离需求 对简单多数据源场景过于重量级
分布式数据库中间件需求 与MyBatis-Plus等框架整合可能存在兼容问题

2.3 Dynamic-datasource

Dynamic-datasource是Baomidou团队开源的多数据源框架,与MyBatis-Plus生态无缝集成。

框架定位

专注于多数据源切换,提供简洁的API和强大的扩展能力,是"小而美"的解决方案。

核心优势

  • 开箱即用:一个注解@DS即可切换数据源
  • 功能丰富:支持动态添加/移除数据源、读写分离、负载均衡
  • 生态融合:与MyBatis-Plus、Seata等无缝集成
  • 轻量级:核心功能简洁,无侵入式设计
  • 社区活跃:Baomidou团队维护,文档完善

2.4 方案对比总结

维度 AbstractRoutingDataSource ShardingSphere Dynamic-datasource
实现成本 高(需自行实现) 中(配置复杂) 低(开箱即用)
功能丰富度
学习成本
灵活性
事务支持 需自行处理 支持XA/本地事务 支持本地事务
动态数据源 需自行实现 支持 原生支持
读写分离 需自行实现 原生支持 原生支持
适用场景 简单多数据源 大规模分片 中小规模多数据源

选型建议

  • 简单的多数据源切换,追求轻量级:Dynamic-datasource
  • 大规模数据分片、分布式数据库需求:ShardingSphere
  • 对框架有深度定制需求、不愿引入第三方依赖:AbstractRoutingDataSource

三、框架认知:Dynamic-datasource核心原理

3.1 整体架构设计

Dynamic-datasource的核心架构围绕DataSource抽象上下文隔离两大机制设计。

核心组件关系

┌─────────────────────────────────────────────────────────────┐
│                    Spring Application                        │
├─────────────────────────────────────────────────────────────┤
│  @DS("xxx")  ──>  DynamicDataSourceAspect  ──>  切面织入     │
│                                                              │
│                        ↓                                     │
│                                                              │
│           DynamicDataSourceContextHolder (ThreadLocal)       │
│                        ↓                                     │
│              DynamicRoutingDataSource                        │
│              (继承 AbstractDataSource)                       │
│                        ↓                                     │
│       ┌────────────────┼────────────────┐                   │
│       ↓                ↓                ↓                   │
│   DataSource_1    DataSource_2    DataSource_N              │
│   (master)         (slave)         (other)                   │
└─────────────────────────────────────────────────────────────┘

DataSource抽象层

框架通过继承AbstractDataSource实现DynamicRoutingDataSource,作为Spring容器的Primary DataSource。所有数据库操作都经过这一层路由到具体的物理数据源。

3.2 核心机制解析

ThreadLocal上下文隔离

框架使用ThreadLocal存储当前线程的数据源标识,确保每个线程的数据源切换互不干扰:

// 核心实现原理(简化版)
public class DynamicDataSourceContextHolder {
    
    // 使用ThreadLocal存储数据源名称
    private static final ThreadLocal<String> LOOKUP_KEY_HOLDER = new ThreadLocal<>();
    
    // 设置当前数据源
    public static void push(String ds) {
        LOOKUP_KEY_HOLDER.set(ds);
    }
    
    // 获取当前数据源
    public static String peek() {
        return LOOKUP_KEY_HOLDER.get();
    }
    
    // 清除数据源(恢复默认)
    public static void poll() {
        LOOKUP_KEY_HOLDER.remove();
    }
}

AOP切面实现原理

框架通过@DS注解配合AOP切面,在方法执行前设置数据源,执行后清理:

// 切面核心逻辑(简化版)
@Aspect
public class DynamicDataSourceAspect {
    
    @Around("@annotation(ds)")
    public Object around(ProceedingJoinPoint point, DS ds) throws Throwable {
        String dsName = ds.value();
        try {
            // 方法执行前:设置数据源
            DynamicDataSourceContextHolder.push(dsName);
            // 执行目标方法
            return point.proceed();
        } finally {
            // 方法执行后:清理数据源
            DynamicDataSourceContextHolder.poll();
        }
    }
}

动态路由决策链

当SQL执行时,路由决策流程如下:

  1. 检查当前线程ThreadLocal中是否有数据源标识
  2. 如有,直接路由到对应数据源
  3. 如无,检查方法/类上是否有@DS注解
  4. 如有注解,使用注解指定的数据源
  5. 如无注解,使用默认数据源(primary)

3.3 关键源码分析

DynamicRoutingDataSource核心逻辑

// 核心路由逻辑(简化版)
public class DynamicRoutingDataSource extends AbstractDataSource {
    
    // 数据源Map:名称 -> DataSource实例
    private Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
    
    // 默认数据源名称
    private String primary = "master";
    
    @Override
    public Connection getConnection() throws SQLException {
        // 获取当前数据源
        DataSource dataSource = determineDataSource();
        return dataSource.getConnection();
    }
    
    // 决定使用哪个数据源
    private DataSource determineDataSource() {
        // 从ThreadLocal获取数据源名称
        String lookupKey = DynamicDataSourceContextHolder.peek();
        if (StringUtils.isEmpty(lookupKey)) {
            lookupKey = this.primary;
        }
        DataSource dataSource = dataSourceMap.get(lookupKey);
        if (dataSource == null) {
            throw new IllegalStateException("数据源 [" + lookupKey + "] 不存在");
        }
        return dataSource;
    }
    
    // 动态添加数据源
    public synchronized void addDataSource(String name, DataSource dataSource) {
        dataSourceMap.put(name, dataSource);
    }
    
    // 动态移除数据源
    public synchronized void removeDataSource(String name) {
        dataSourceMap.remove(name);
    }
}

@DS注解定义

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
    /**
     * 数据源名称
     * 可以是具体的名称,也可以是SpEL表达式
     */
    String value() default "";
}

注解优先级处理

// 注解解析器(简化版)
public class DsProcessor {
    
    // 方法级别注解 > 类级别注解 > 默认数据源
    public String determineDs(MethodInvocation invocation) {
        Method method = invocation.getMethod();
        Class<?> targetClass = invocation.getThis().getClass();
        
        // 1. 优先查找方法级别注解
        DS methodDs = method.getAnnotation(DS.class);
        if (methodDs != null) {
            return methodDs.value();
        }
        
        // 2. 其次查找类级别注解
        DS classDs = targetClass.getAnnotation(DS.class);
        if (classDs != null) {
            return classDs.value();
        }
        
        // 3. 返回null,使用默认数据源
        return null;
    }
}

四、基础实践:快速集成与使用

4.1 Maven依赖配置

<dependencies>
    <!-- Dynamic-datasource 核心依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>4.3.0</version>
    </dependency>
    
    <!-- MyBatis-Plus(推荐搭配使用) -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5</version>
    </dependency>
    
    <!-- 数据库驱动(以MySQL为例) -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- 连接池(HikariCP,Spring Boot默认包含) -->
    <!-- 如需指定版本,可显式引入 -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.0.1</version>
    </dependency>
</dependencies>

版本兼容说明

Dynamic-datasource Spring Boot MyBatis-Plus
4.3.0 3.x 3.5.x
4.2.0 2.7.x 3.5.x
3.6.1 2.3.x 3.4.x

4.2 YAML配置详解

基础配置示例

spring:
  datasource:
    dynamic:
      # 主数据源名称(默认数据源)
      primary: master
      # 严格模式:未配置的数据源会抛出异常(生产环境建议true)
      strict: true
      # 数据源配置
      datasource:
        # 主库
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: root
        # 订单库
        order:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: root
        # 库存库
        inventory:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/inventory_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: root

连接池参数配置

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/master_db
          username: root
          password: root
          # HikariCP连接池配置
          hikari:
            # 最小空闲连接数
            min-idle: 5
            # 最大连接数
            max-pool-size: 20
            # 连接超时时间(毫秒)
            connection-timeout: 30000
            # 空闲连接存活最大时间(毫秒)
            idle-timeout: 600000
            # 连接最大存活时间(毫秒)
            max-lifetime: 1800000
            # 连接测试查询
            connection-test-query: SELECT 1
        order:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/order_db
          username: root
          password: root
          hikari:
            min-idle: 3
            max-pool-size: 15

多环境配置示例

# application.yml(通用配置)
spring:
  datasource:
    dynamic:
      primary: master
      strict: false  # 开发环境关闭严格模式

# application-dev.yml(开发环境)
spring:
  datasource:
    dynamic:
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/dev_master_db
          username: dev_user
          password: dev_pass
        order:
          url: jdbc:mysql://localhost:3306/dev_order_db
          username: dev_user
          password: dev_pass

# application-prod.yml(生产环境)
spring:
  datasource:
    dynamic:
      strict: true  # 生产环境开启严格模式
      datasource:
        master:
          url: ${MASTER_DB_URL}
          username: ${MASTER_DB_USERNAME}
          password: ${MASTER_DB_PASSWORD}
          hikari:
            min-idle: 10
            max-pool-size: 50
        order:
          url: ${ORDER_DB_URL}
          username: ${ORDER_DB_USERNAME}
          password: ${ORDER_DB_PASSWORD}
          hikari:
            min-idle: 5
            max-pool-size: 30

4.3 @DS注解使用指南

导入注解import com.baomidou.dynamic.datasource.annotation.DS;

注解优先级规则方法级别 @DS > 类级别 @DS > 默认数据源(primary)

综合示例

@Service
@DS("order")  // 类级别注解:所有方法默认使用订单库
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    // 使用 order 数据源(继承类级别注解)
    public Order getOrderById(Long orderId) {
        return orderMapper.selectById(orderId);
    }
    
    // 使用 inventory 数据源(方法级别优先,覆盖类级别)
    @DS("inventory")
    public Inventory getInventory(Long productId) {
        return inventoryMapper.selectById(productId);
    }
    
    // 使用 master 数据源(空字符串表示主数据源)
    @DS("")
    public void saveOrder(Order order) {
        orderMapper.insert(order);
    }
}

4.4 手动切换场景

在某些场景下,无法使用注解方式(如动态决定数据源名称),需要手动切换。

DynamicDataSourceContextHolder API

import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;

@Service
public class DynamicQueryService {
    
    /**
     * 手动切换数据源示例
     */
    public Object queryFromDynamicSource(String dsName, Long id) {
        try {
            // 切换到指定数据源
            DynamicDataSourceContextHolder.push(dsName);
            
            // 执行查询操作
            return someMapper.selectById(id);
            
        } finally {
            // 清除数据源,恢复默认
            // 重要:必须在finally块中调用,确保清除
            DynamicDataSourceContextHolder.poll();
        }
    }
    
    /**
     * 获取当前数据源名称
     */
    public String getCurrentDataSource() {
        return DynamicDataSourceContextHolder.peek();
    }
}

多租户动态切换示例

@Service
public class TenantService {
    
    @Autowired
    private UserService userService;
    
    /**
     * 根据租户ID动态切换数据源
     */
    public User getUserByTenant(Long tenantId, Long userId) {
        String dsName = "tenant_" + tenantId;
        
        try {
            DynamicDataSourceContextHolder.push(dsName);
            return userService.getById(userId);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
}

适用场景

场景 推荐方式
数据源固定 注解方式
数据源动态(运行时决定) 手动切换
需要嵌套切换 手动切换
多租户场景 手动切换或自定义注解处理器

SpEL表达式动态数据源

Dynamic-datasource支持在@DS注解中使用SpEL表达式,实现动态数据源计算。

基础示例

@Service
public class TenantAwareService {
    
    // 从方法参数获取租户ID:#tenantId 表示取参数值
    @DS("#tenantId")
    public Order getOrderByTenant(Long tenantId, Long orderId) {
        return orderMapper.selectById(orderId);
    }
    
    // 分片计算:根据订单ID计算分片数据源
    @DS("'shard_' + #orderId % 3")
    public Order getOrderFromShard(Long orderId) {
        return orderMapper.selectById(orderId);
    }
    
    // 按月分库:根据日期选择数据源
    @DS("'archive_' + #date.year + '_' + #date.monthValue")
    public List<Order> listArchiveOrders(LocalDate date) {
        return archiveOrderMapper.selectByMonth(date);
    }
}
表达式 说明 示例
#paramName 方法参数引用 @DS("#tenantId")
#this 当前对象引用 @DS("#this.dataSource")
T(Type).method() 静态方法调用 @DS("T(Math).abs(#id)")
'prefix_' + #var 字符串拼接 @DS("'shard_' + #id % 3")
#obj.property 对象属性访问 @DS("#order.tenantId")
#obj.method() 对象方法调用 @DS("#context.getDs()")

注意事项

  • 必须在finally块中清除数据源
  • 避免在异步线程中忘记清除
  • 手动切换与注解方式可以混用,但需注意优先级
  • SpEL表达式计算结果必须与配置的数据源名称匹配(strict=true时会校验)

4.5 版本兼容性与升级指南

Dynamic-datasource框架经历了多个版本迭代,了解版本差异对于项目升级至关重要。

版本演进概览

版本 发布时间 主要变更 推荐使用
4.3.x 2024年 支持Spring Boot 3.x,优化Seata集成 ✅ 新项目推荐
4.2.x 2023年 支持Spring Boot 2.7.x,增强动态数据源 ✅ 稳定版本
3.6.x 2022年 支持Spring Boot 2.3-2.6 ⚠️ 维护阶段
3.5.x及以下 2021年及之前 基础版本 ❌ 不推荐

Spring Boot版本适配

# Spring Boot 3.x 推荐配置
spring:
  datasource:
    dynamic:
      primary: master
      # Spring Boot 3.x 需使用4.3.0及以上版本

# Spring Boot 2.7.x 推荐配置
spring:
  datasource:
    dynamic:
      primary: master
      # 推荐使用4.2.0版本

3.x到4.x升级注意事项

// 3.x版本API(已废弃)
DynamicDataSourceContextHolder.setDataSource("order");  // 已废弃
DynamicDataSourceContextHolder.getDataSource();         // 已废弃
DynamicDataSourceContextHolder.clearDataSource();       // 已废弃

// 4.x版本API(推荐)
DynamicDataSourceContextHolder.push("order");    // 新API
DynamicDataSourceContextHolder.peek();           // 新API
DynamicDataSourceContextHolder.poll();           // 新API

升级迁移步骤

# 1. 更新Maven依赖版本
# 将dynamic-datasource-spring-boot-starter版本从3.x升级到4.x

# 2. 替换废弃API调用
# 搜索并替换setDataSource -> push
# 搜索并替换getDataSource -> peek
# 搜索并替换clearDataSource -> poll

# 3. 检查配置文件兼容性
# 4.x版本对某些配置属性名称有调整

常见升级问题及解决方案

问题 原因 解决方案
启动报错"DataSource not found" strict模式默认开启 检查数据源名称配置,或在开发环境设置strict=false
API调用编译错误 使用了废弃方法 替换为push/peek/poll新API
Seata集成失效 Seata版本不匹配 Seata需使用2.0.0及以上版本
MyBatis-Plus分页异常 版本兼容问题 MyBatis-Plus升级到3.5.x

本节小结:基础实践部分涵盖了Maven依赖配置、YAML配置详解、@DS注解使用、手动切换API以及版本兼容性指南,为后续进阶实践奠定了基础。


五、进阶实践:高阶能力解锁

5.1 动态数据源管理

Dynamic-datasource支持在运行时动态添加和移除数据源,适用于多租户等场景。

动态添加数据源

@Service
public class DynamicDataSourceService {
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    
    /**
     * 动态添加数据源
     * @param dsName 数据源名称
     * @param url 数据库URL
     * @param username 用户名
     * @param password 密码
     */
    public void addDataSource(String dsName, String url, String username, String password) {
        // 创建数据源配置
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("com.mysql.cj.jdbc.Driver");
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        config.setMinimumIdle(2);
        config.setMaximumPoolSize(10);
        config.setConnectionTimeout(30000);
        config.setIdleTimeout(600000);
        config.setMaxLifetime(1800000);
        config.setConnectionTestQuery("SELECT 1");
        
        // 创建数据源实例
        HikariDataSource dataSource = new HikariDataSource(config);
        
        // 添加到动态数据源管理器
        dynamicRoutingDataSource.addDataSource(dsName, dataSource);
        
        // 或者使用 DataSourceProperty 方式
        // DataSourceProperty property = new DataSourceProperty();
        // property.setUrl(url);
        // property.setUsername(username);
        // property.setPassword(password);
        // property.setDriverClassName("com.mysql.cj.jdbc.Driver");
        // dynamicRoutingDataSource.addDataSource(dsName, property);
    }
}

动态移除数据源

@Service
public class DynamicDataSourceService {
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    
    /**
     * 动态移除数据源
     * @param dsName 数据源名称
     */
    public void removeDataSource(String dsName) {
        // 检查数据源是否存在
        if (!dynamicRoutingDataSource.getDataSources().containsKey(dsName)) {
            throw new IllegalArgumentException("数据源 [" + dsName + "] 不存在");
        }
        
        // 移除数据源
        DataSource removed = dynamicRoutingDataSource.removeDataSource(dsName);
        
        // 关闭数据源连接池
        if (removed instanceof HikariDataSource) {
            ((HikariDataSource) removed).close();
        }
    }
    
    /**
     * 获取所有数据源名称
     */
    public Set<String> listDataSources() {
        return dynamicRoutingDataSource.getDataSources().keySet();
    }
}

多租户动态切换实战案例

/**
 * 多租户数据源管理器
 */
@Service
@Slf4j
public class TenantDataSourceManager {
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    
    @Autowired
    private TenantConfigRepository tenantConfigRepository;
    
    /**
     * 初始化租户数据源(应用启动时调用)
     */
    @PostConstruct
    public void initTenantDataSources() {
        List<TenantConfig> tenants = tenantConfigRepository.findAllActiveTenants();
        
        for (TenantConfig tenant : tenants) {
            try {
                addTenantDataSource(tenant);
                log.info("初始化租户数据源成功: {}", tenant.getTenantId());
            } catch (Exception e) {
                log.error("初始化租户数据源失败: {}", tenant.getTenantId(), e);
            }
        }
    }
    
    /**
     * 添加租户数据源
     */
    public void addTenantDataSource(TenantConfig tenant) {
        String dsName = "tenant_" + tenant.getTenantId();
        
        DataSourceProperty property = new DataSourceProperty();
        property.setUrl(tenant.getDbUrl());
        property.setUsername(tenant.getDbUsername());
        property.setPassword(tenant.getDbPassword());
        property.setDriverClassName("com.mysql.cj.jdbc.Driver");
        
        // 配置连接池
        HikariCpConfig hikari = new HikariCpConfig();
        hikari.setMinimumIdle(2);
        hikari.setMaximumPoolSize(10);
        property.setHikari(hikari);
        
        dynamicRoutingDataSource.addDataSource(dsName, property);
    }
    
    /**
     * 移除租户数据源
     */
    public void removeTenantDataSource(Long tenantId) {
        String dsName = "tenant_" + tenantId;
        
        if (dynamicRoutingDataSource.getDataSources().containsKey(dsName)) {
            DataSource removed = dynamicRoutingDataSource.removeDataSource(dsName);
            // 关闭连接池
            if (removed instanceof AutoCloseable) {
                try {
                    ((AutoCloseable) removed).close();
                } catch (Exception e) {
                    log.error("关闭数据源失败: {}", dsName, e);
                }
            }
        }
    }
    
    /**
     * 切换到指定租户数据源
     */
    public void switchToTenant(Long tenantId) {
        String dsName = "tenant_" + tenantId;
        
        if (!dynamicRoutingDataSource.getDataSources().containsKey(dsName)) {
            throw new IllegalArgumentException("租户数据源不存在: " + tenantId);
        }
        
        DynamicDataSourceContextHolder.push(dsName);
    }
    
    /**
     * 清除租户数据源
     */
    public void clearTenant() {
        DynamicDataSourceContextHolder.poll();
    }
}

/**
 * 租户上下文工具类
 */
public class TenantContextHolder {
    
    private static final ThreadLocal<Long> TENANT_ID = new ThreadLocal<>();
    
    public static void setTenantId(Long tenantId) {
        TENANT_ID.set(tenantId);
    }
    
    public static Long getTenantId() {
        return TENANT_ID.get();
    }
    
    public static void clear() {
        TENANT_ID.remove();
    }
}

/**
 * 租户切换切面
 */
@Aspect
@Component
public class TenantAspect {
    
    @Autowired
    private TenantDataSourceManager tenantDataSourceManager;
    
    @Around("@annotation(tenant)")
    public Object around(ProceedingJoinPoint point, Tenant tenant) throws Throwable {
        Long tenantId = TenantContextHolder.getTenantId();
        if (tenantId == null) {
            throw new IllegalStateException("未设置租户ID");
        }
        
        try {
            tenantDataSourceManager.switchToTenant(tenantId);
            return point.proceed();
        } finally {
            tenantDataSourceManager.clearTenant();
        }
    }
}

// 使用示例
@RestController
@RequestMapping("/api")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @GetMapping("/orders/{orderId}")
    public Order getOrder(@PathVariable Long orderId, @RequestHeader("X-Tenant-Id") Long tenantId) {
        try {
            TenantContextHolder.setTenantId(tenantId);
            return orderService.getOrderById(orderId);
        } finally {
            TenantContextHolder.clear();
        }
    }
}

5.2 读写分离与负载均衡

主从配置示例

spring:
  datasource:
    dynamic:
      primary: master
      strict: false
      datasource:
        # 主库(写库)
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://master-db:3306/business_db
          username: root
          password: root
        # 从库1(读库)
        slave_1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://slave-db-1:3306/business_db
          username: root
          password: root
        # 从库2(读库)
        slave_2:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://slave-db-2:3306/business_db
          username: root
          password: root

读写分离使用方式

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 写操作 - 使用主库
     */
    @DS("master")
    public void createUser(User user) {
        userMapper.insert(user);
    }
    
    /**
     * 读操作 - 使用从库
     * 可以指定具体的从库,也可以使用负载均衡
     */
    @DS("slave_1")
    public User getUserById(Long userId) {
        return userMapper.selectById(userId);
    }
    
    /**
     * 批量查询 - 使用另一个从库分担压力
     */
    @DS("slave_2")
    public List<User> listUsers() {
        return userMapper.selectList(null);
    }
}

使用@DSTransactional实现读写分离事务

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 使用@DSTransactional注解
     * 在事务开始时切换到主库,事务结束后恢复
     */
    @DSTransactional
    @DS("master")  // 确保使用主库
    public void createOrder(Order order) {
        // 插入订单
        orderMapper.insert(order);
        // 扣减库存
        inventoryMapper.decreaseStock(order.getProductId(), order.getQuantity());
    }
}

5.3 多数据源事务处理

单数据源事务

单数据源事务使用Spring的@Transactional即可:

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * 单数据源事务
     */
    @Transactional(rollbackFor = Exception.class)
    @DS("order")
    public void updateOrder(Order order) {
        Order existingOrder = orderMapper.selectById(order.getId());
        existingOrder.setStatus(order.getStatus());
        orderMapper.updateById(existingOrder);
    }
}

跨数据源事务的挑战

跨数据源事务存在以下问题:

  1. Spring的@Transactional只能管理单个数据源的事务
  2. 多个数据源的事务无法统一提交/回滚
  3. 可能出现部分成功、部分失败的不一致状态

解决方案一:编程式事务管理

@Service
public class CrossDataSourceService {
    
    @Autowired
    private DataSourceTransactionManager masterTransactionManager;
    
    @Autowired
    private DataSourceTransactionManager orderTransactionManager;
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 编程式事务管理
     * 每个数据源独立事务,需要手动处理一致性
     */
    public void createOrderWithInventory(Order order) {
        // 事务定义
        TransactionDefinition definition = new DefaultTransactionDefinition();
        
        // 开启订单库事务
        TransactionStatus orderTx = orderTransactionManager.getTransaction(definition);
        // 开启库存库事务
        TransactionStatus inventoryTx = masterTransactionManager.getTransaction(definition);
        
        try {
            // 操作订单库
            orderMapper.insert(order);
            
            // 操作库存库
            inventoryMapper.decreaseStock(order.getProductId(), order.getQuantity());
            
            // 提交库存库事务
            masterTransactionManager.commit(inventoryTx);
            // 提交订单库事务
            orderTransactionManager.commit(orderTx);
            
        } catch (Exception e) {
            // 回滚两个事务
            orderTransactionManager.rollback(orderTx);
            masterTransactionManager.rollback(inventoryTx);
            throw new RuntimeException("创建订单失败", e);
        }
    }
}

解决方案二:Seata分布式事务

对于严格一致性的跨数据源事务,建议使用Seata:

<!-- 添加Seata依赖 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>4.3.0</version>
</dependency>
# Seata配置
seata:
  enabled: true
  application-id: business-service
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: ""
      group: SEATA_GROUP

# Dynamic-datasource配置
spring:
  datasource:
    dynamic:
      seata: true  # 开启Seata支持
      seata-mode: AT  # AT模式
      primary: master
      datasource:
        master:
          # ... 配置省略
        order:
          # ... 配置省略
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 使用Seata分布式事务
     * @GlobalTransactional 保证跨数据源事务一致性
     */
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 操作订单库
        orderMapper.insert(order);
        
        // 操作库存库
        inventoryMapper.decreaseStock(order.getProductId(), order.getQuantity());
        
        // Seata自动管理跨数据源事务的提交/回滚
    }
}

5.4 数据分片场景实践

Dynamic-datasource框架本身不提供分片功能,但可以与分库分表方案配合使用,或通过数据源分组实现简单的分片路由。

分片场景概述

分片类型 描述 Dynamic-datasource适用性
分库 数据按规则分散到多个数据库 ✅ 支持多数据源路由
分表 同库内数据分散到多个表 ⚠️ 需配合MyBatis动态表名
分库+分表 数据分散到多库多表 ⚠️ 建议使用ShardingSphere

基于数据源的分库方案

# 按业务ID分库配置示例
spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        # 分片库配置
        shard_0:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://shard-server-1:3306/shard_db_0
          username: root
          password: root
        shard_1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://shard-server-2:3306/shard_db_1
          username: root
          password: root
        shard_2:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://shard-server-3:3306/shard_db_2
          username: root
          password: root

分片路由策略实现

/**
 * 分片路由工具类
 */
public class ShardRouter {
    
    // 分片数量
    private static final int SHARD_COUNT = 3;
    
    /**
     * 根据业务ID计算分片索引
     * @param businessId 业务ID
     * @return 分片数据源名称
     */
    public static String routeByBusinessId(Long businessId) {
        int shardIndex = (int) (businessId % SHARD_COUNT);
        return "shard_" + shardIndex;
    }
    
    /**
     * 根据用户ID哈希分片
     * @param userId 用户ID
     * @return 分片数据源名称
     */
    public static String routeByUserId(Long userId) {
        int hash = Math.abs(userId.hashCode());
        int shardIndex = hash % SHARD_COUNT;
        return "shard_" + shardIndex;
    }
    
    /**
     * 按时间范围分片(按月)
     * @param date 日期
     * @return 分片数据源名称
     */
    public static String routeByMonth(LocalDate date) {
        int monthValue = date.getMonthValue();
        int shardIndex = monthValue % SHARD_COUNT;
        return "shard_" + shardIndex;
    }
}

/**
 * 分片查询服务
 */
@Service
public class ShardOrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * 根据订单ID查询(自动路由到对应分片)
     */
    public Order getOrderById(Long orderId) {
        String shardDs = ShardRouter.routeByBusinessId(orderId);
        
        try {
            DynamicDataSourceContextHolder.push(shardDs);
            return orderMapper.selectById(orderId);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
    
    /**
     * 创建订单(路由到对应分片)
     */
    public void createOrder(Order order) {
        String shardDs = ShardRouter.routeByBusinessId(order.getId());
        
        try {
            DynamicDataSourceContextHolder.push(shardDs);
            orderMapper.insert(order);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
    
    /**
     * 按用户ID查询订单列表
     */
    public List<Order> listOrdersByUserId(Long userId) {
        String shardDs = ShardRouter.routeByUserId(userId);
        
        try {
            DynamicDataSourceContextHolder.push(shardDs);
            LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(Order::getUserId, userId);
            return orderMapper.selectList(wrapper);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
}

动态表名分表方案

/**
 * 动态表名处理器
 * 配合MyBatis-Plus实现同库分表
 */
@Component
public class DynamicTableNameHandler {
    
    /**
     * 获取分表名称
     * @param baseTableName 基础表名
     * @param shardKey 分片键
     * @return 实际表名
     */
    public static String getShardTableName(String baseTableName, Long shardKey) {
        int tableIndex = (int) (shardKey % 10);  // 10张分表
        return baseTableName + "_" + tableIndex;
    }
    
    /**
     * 获取按月的分表名称
     * @param baseTableName 庺础表名
     * @param date 日期
     * @return 实际表名
     */
    public static String getMonthTableName(String baseTableName, LocalDate date) {
        String monthStr = date.format(DateTimeFormatter.ofPattern("yyyyMM"));
        return baseTableName + "_" + monthStr;
    }
}

/**
 * 动态表名Mapper示例
 */
public interface OrderLogMapper extends BaseMapper<OrderLog> {
    
    /**
     * 动态表名查询
     * @param tableName 实际表名
     * @param wrapper 查询条件
     * @return 结果列表
     */
    @Select("SELECT * FROM ${tableName} WHERE user_id = #{userId}")
    List<OrderLog> selectByDynamicTable(@Param("tableName") String tableName, 
                                        @Param("userId") Long userId);
}

/**
 * 分表查询服务
 */
@Service
public class OrderLogService {
    
    @Autowired
    private OrderLogMapper orderLogMapper;
    
    /**
     * 查询订单日志(分库+分表)
     */
    public List<OrderLog> getOrderLogs(Long orderId, LocalDate date) {
        // 1. 确定分库
        String shardDs = ShardRouter.routeByBusinessId(orderId);
        // 2. 确定分表
        String tableName = DynamicTableNameHandler.getMonthTableName("order_log", date);
        
        try {
            DynamicDataSourceContextHolder.push(shardDs);
            return orderLogMapper.selectByDynamicTable(tableName, orderId);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
}

分片场景注意事项

// ❌ 错误:跨分片查询不使用聚合
@Service
public class WrongShardService {
    
    /**
     * 错误示例:直接查询所有分片
     * 会导致数据不完整或路由错误
     */
    public List<Order> listAllOrders() {
        // 无法在一个数据源上查询所有分片数据
        return orderMapper.selectList(null);  // 只能查到一个分片
    }
}

// ✅ 正确:跨分片查询使用聚合服务
@Service
public class CorrectShardService {
    
    /**
     * 正确示例:遍历所有分片聚合查询
     */
    public List<Order> listAllOrders() {
        List<Order> allOrders = new ArrayList<>();
        
        // 遍历所有分片
        for (int i = 0; i < 3; i++) {
            String shardDs = "shard_" + i;
            try {
                DynamicDataSourceContextHolder.push(shardDs);
                List<Order> shardOrders = orderMapper.selectList(null);
                allOrders.addAll(shardOrders);
            } finally {
                DynamicDataSourceContextHolder.poll();
            }
        }
        
        return allOrders;
    }
}

5.5 跨数据源查询聚合

在多数据源场景下,经常需要从不同数据源查询数据并进行聚合处理,这是进阶使用的重要能力。

跨数据源聚合场景

场景 描述 挑战
用户-订单聚合 用户库+订单库联合查询 数据分散,无法JOIN
商品-库存聚合 商品库+库存库联合查询 需要内存拼接
报表统计 多业务库数据汇总 性能优化、分页处理
数据迁移校验 新旧库数据比对 数据一致性验证

基础聚合模式

/**
 * 跨数据源聚合服务
 */
@Service
public class UserOrderAggregateService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * 基础聚合:用户+订单信息组合
     */
    public UserOrderDTO getUserWithOrders(Long userId) {
        // 1. 查询用户信息(用户库)
        User user;
        try {
            DynamicDataSourceContextHolder.push("user");
            user = userMapper.selectById(userId);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        if (user == null) {
            return null;
        }
        
        // 2. 查询订单信息(订单库)
        List<Order> orders;
        try {
            DynamicDataSourceContextHolder.push("order");
            LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(Order::getUserId, userId);
            orders = orderMapper.selectList(wrapper);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 3. 内存聚合
        UserOrderDTO dto = new UserOrderDTO();
        dto.setUser(user);
        dto.setOrders(orders);
        dto.setOrderCount(orders.size());
        dto.setTotalAmount(orders.stream()
            .map(Order::getAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add));
        
        return dto;
    }
}

并行查询优化

/**
 * 并行聚合服务(提升查询性能)
 */
@Service
public class ParallelAggregateService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 并行查询多数据源,提升性能
     */
    public CompletableFuture<OrderDetailDTO> getOrderDetailAsync(Long orderId) {
        // 异步查询订单
        CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> {
            try {
                DynamicDataSourceContextHolder.push("order");
                return orderMapper.selectById(orderId);
            } finally {
                DynamicDataSourceContextHolder.poll();
            }
        });
        
        // 异步查询商品信息
        CompletableFuture<Product> productFuture = orderFuture.thenApplyAsync(order -> {
            if (order == null) return null;
            try {
                DynamicDataSourceContextHolder.push("product");
                return productMapper.selectById(order.getProductId());
            } finally {
                DynamicDataSourceContextHolder.poll();
            }
        });
        
        // 异步查询库存信息
        CompletableFuture<Inventory> inventoryFuture = orderFuture.thenApplyAsync(order -> {
            if (order == null) return null;
            try {
                DynamicDataSourceContextHolder.push("inventory");
                return inventoryMapper.selectByProductId(order.getProductId());
            } finally {
                DynamicDataSourceContextHolder.poll();
            }
        });
        
        // 异步查询用户信息
        CompletableFuture<User> userFuture = orderFuture.thenApplyAsync(order -> {
            if (order == null) return null;
            try {
                DynamicDataSourceContextHolder.push("user");
                return userMapper.selectById(order.getUserId());
            } finally {
                DynamicDataSourceContextHolder.poll();
            }
        });
        
        // 等待所有查询完成,聚合结果
        return CompletableFuture.allOf(productFuture, inventoryFuture, userFuture)
            .thenApply(v -> {
                Order order = orderFuture.join();
                if (order == null) return null;
                
                OrderDetailDTO dto = new OrderDetailDTO();
                dto.setOrder(order);
                dto.setProduct(productFuture.join());
                dto.setInventory(inventoryFuture.join());
                dto.setUser(userFuture.join());
                
                return dto;
            });
    }
    
    /**
     * 同步方式调用并行聚合
     */
    public OrderDetailDTO getOrderDetail(Long orderId) {
        try {
            return getOrderDetailAsync(orderId).get(10, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            throw new RuntimeException("查询超时");
        } catch (Exception e) {
            throw new RuntimeException("查询失败", e);
        }
    }
}

分页聚合处理

/**
 * 跨数据源分页聚合
 */
@Service
public class PaginatedAggregateService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 跨库分页查询(订单+用户信息)
     * 分页在订单库,用户信息补充查询
     */
    public IPage<OrderUserDTO> pageOrdersWithUser(int pageNum, int pageSize) {
        // 1. 从订单库分页查询订单
        Page<Order> orderPage;
        try {
            DynamicDataSourceContextHolder.push("order");
            orderPage = orderMapper.selectPage(new Page<>(pageNum, pageSize), null);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 2. 收集所有用户ID
        Set<Long> userIds = orderPage.getRecords().stream()
            .map(Order::getUserId)
            .collect(Collectors.toSet());
        
        // 3. 批量查询用户信息
        Map<Long, User> userMap;
        try {
            DynamicDataSourceContextHolder.push("user");
            List<User> users = userMapper.selectBatchIds(userIds);
            userMap = users.stream()
                .collect(Collectors.toMap(User::getId, Function.identity()));
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 4. 组装分页结果
        List<OrderUserDTO> dtoList = orderPage.getRecords().stream()
            .map(order -> {
                OrderUserDTO dto = new OrderUserDTO();
                dto.setOrder(order);
                dto.setUser(userMap.get(order.getUserId()));
                return dto;
            })
            .collect(Collectors.toList());
        
        // 5. 构造分页返回对象
        Page<OrderUserDTO> resultPage = new Page<>(pageNum, pageSize);
        resultPage.setRecords(dtoList);
        resultPage.setTotal(orderPage.getTotal());
        resultPage.setPages(orderPage.getPages());
        
        return resultPage;
    }
}

报表统计聚合

/**
 * 跨库报表统计服务
 */
@Service
public class ReportAggregateService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private PaymentMapper paymentMapper;
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 多库数据汇总报表
     */
    public BusinessReportDTO generateDailyReport(LocalDate date) {
        BusinessReportDTO report = new BusinessReportDTO();
        report.setDate(date);
        
        // 1. 订单统计(订单库)
        try {
            DynamicDataSourceContextHolder.push("order");
            
            // 日订单总数
            LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
            wrapper.between(Order::getCreateTime, 
                date.atStartOfDay(), date.plusDays(1).atStartOfDay());
            Long orderCount = orderMapper.selectCount(wrapper);
            report.setOrderCount(orderCount);
            
            // 日销售额
            List<Order> orders = orderMapper.selectList(wrapper);
            BigDecimal totalAmount = orders.stream()
                .map(Order::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
            report.setTotalAmount(totalAmount);
            
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 2. 支付统计(支付库)
        try {
            DynamicDataSourceContextHolder.push("payment");
            
            LambdaQueryWrapper<Payment> wrapper = new LambdaQueryWrapper<>();
            wrapper.between(Payment::getCreateTime,
                date.atStartOfDay(), date.plusDays(1).atStartOfDay());
            Long paymentCount = paymentMapper.selectCount(wrapper);
            report.setPaymentCount(paymentCount);
            
            // 支付金额
            List<Payment> payments = paymentMapper.selectList(wrapper);
            BigDecimal totalPayment = payments.stream()
                .filter(p -> "SUCCESS".equals(p.getStatus()))
                .map(Payment::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
            report.setTotalPayment(totalPayment);
            
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 3. 库存统计(库存库)
        try {
            DynamicDataSourceContextHolder.push("inventory");
            
            // 库存总量
            Long totalStock = inventoryMapper.sumTotalStock();
            report.setTotalStock(totalStock);
            
            // 低库存商品数
            Long lowStockCount = inventoryMapper.countLowStock(10);
            report.setLowStockCount(lowStockCount);
            
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 4. 计算衍生指标
        report.setPaymentRate(report.getOrderCount() > 0 
            ? (double) report.getPaymentCount() / report.getOrderCount() 
            : 0);
        report.setAvgOrderAmount(report.getOrderCount() > 0 
            ? report.getTotalAmount().divide(new BigDecimal(report.getOrderCount()), 2, RoundingMode.HALF_UP)
            : BigDecimal.ZERO);
        
        return report;
    }
}

数据一致性校验

/**
 * 跨库数据校验服务
 * 用于数据迁移、同步场景
 */
@Service
public class DataVerifyService {
    
    @Autowired
    private OldOrderMapper oldOrderMapper;  // 旧库
    
    @Autowired
    private NewOrderMapper newOrderMapper;  // 新库
    
    /**
     * 数据迁移校验
     */
    public DataVerifyResult verifyMigration(LocalDate migrateDate) {
        DataVerifyResult result = new DataVerifyResult();
        
        // 1. 从旧库查询迁移范围内的数据总数
        Long oldCount;
        List<Order> oldOrders;
        try {
            DynamicDataSourceContextHolder.push("old_system");
            
            LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
            wrapper.between(Order::getCreateTime,
                migrateDate.atStartOfDay(), migrateDate.plusDays(1).atStartOfDay());
            oldCount = oldOrderMapper.selectCount(wrapper);
            oldOrders = oldOrderMapper.selectList(wrapper);
            
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 2. 从新库查询已迁移数据总数
        Long newCount;
        List<Order> newOrders;
        try {
            DynamicDataSourceContextHolder.push("new_system");
            
            LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
            wrapper.between(Order::getCreateTime,
                migrateDate.atStartOfDay(), migrateDate.plusDays(1).atStartOfDay());
            newCount = newOrderMapper.selectCount(wrapper);
            newOrders = newOrderMapper.selectList(wrapper);
            
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
        
        // 3. 数量对比
        result.setOldCount(oldCount);
        result.setNewCount(newCount);
        result.setCountMatch(oldCount.equals(newCount));
        
        // 4. 数据内容对比
        Map<Long, Order> oldMap = oldOrders.stream()
            .collect(Collectors.toMap(Order::getId, Function.identity()));
        Map<Long, Order> newMap = newOrders.stream()
            .collect(Collectors.toMap(Order::getId, Function.identity()));
        
        List<DataDiff> diffs = new ArrayList<>();
        for (Order oldOrder : oldOrders) {
            Order newOrder = newMap.get(oldOrder.getId());
            if (newOrder == null) {
                diffs.add(new DataDiff(oldOrder.getId(), "MISSING", "新库不存在"));
            } else {
                // 对比关键字段
                if (!Objects.equals(oldOrder.getAmount(), newOrder.getAmount())) {
                    diffs.add(new DataDiff(oldOrder.getId(), "AMOUNT_DIFF",
                        "旧:" + oldOrder.getAmount() + ", 新:" + newOrder.getAmount()));
                }
                if (!Objects.equals(oldOrder.getStatus(), newOrder.getStatus())) {
                    diffs.add(new DataDiff(oldOrder.getId(), "STATUS_DIFF",
                        "旧:" + oldOrder.getStatus() + ", 新:" + newOrder.getStatus()));
                }
            }
        }
        
        result.setDiffs(diffs);
        result.setDataMatch(diffs.isEmpty());
        
        return result;
    }
}

聚合查询性能优化建议

优化策略 描述 适用场景
并行查询 CompletableFuture异步并行 多数据源独立查询
批量查询 批量ID查询替代逐条查询 关联数据补全
缓存辅助 热点数据Redis缓存 用户信息等高频查询
分页优先 分页在主库,关联数据补充 大数据量展示
预聚合 定时任务预计算聚合结果 报表统计场景

5.6 MyBatis-Plus深度整合

配置整合

@Configuration
@MapperScan("com.example.mapper")
public class MybatisPlusConfig {
    
    /**
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 防全表更新/删除插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

多数据源分页示例

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 订单库分页查询
     */
    @DS("order")
    public IPage<Order> pageOrders(int pageNum, int pageSize) {
        Page<Order> page = new Page<>(pageNum, pageSize);
        return orderMapper.selectPage(page, null);
    }
    
    /**
     * 库存库分页查询
     */
    @DS("master")
    public IPage<Inventory> pageInventories(int pageNum, int pageSize) {
        Page<Inventory> page = new Page<>(pageNum, pageSize);
        return inventoryMapper.selectPage(page, null);
    }
}

乐观锁使用注意事项

// 实体类
@Data
@TableName("t_product")
public class Product {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    
    private String name;
    
    private Integer stock;
    
    @Version  // 乐观锁版本号字段
    private Integer version;
}

// Service
@Service
public class ProductService {
    
    @Autowired
    private ProductMapper productMapper;
    
    /**
     * 扣减库存(使用乐观锁)
     * 注意:乐观锁需要在同一个数据源中操作
     */
    @DS("master")
    @Transactional(rollbackFor = Exception.class)
    public boolean decreaseStock(Long productId, Integer quantity) {
        Product product = productMapper.selectById(productId);
        if (product.getStock() < quantity) {
            throw new RuntimeException("库存不足");
        }
        
        product.setStock(product.getStock() - quantity);
        // MyBatis-Plus会自动处理版本号
        return productMapper.updateById(product) > 0;
    }
}

六、避坑指南:常见问题与解决方案

6.1 数据源切换不生效

问题描述

使用@DS注解后,数据源切换未生效,SQL仍在默认数据源执行。

问题根因分析

原因 描述
注解位置错误 @DS注解在private方法上无效
AOP代理问题 同类方法调用,绕过代理
数据源名称错误 配置文件中不存在该数据源
strict模式 strict=false时,不存在的数据源会回退到默认

排查步骤

// 1. 检查当前数据源
String currentDs = DynamicDataSourceContextHolder.peek();
log.info("当前数据源: {}", currentDs);

// 2. 检查已配置的数据源
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;

Set<String> dsNames = dynamicRoutingDataSource.getDataSources().keySet();
log.info("已配置数据源: {}", dsNames);

解决方案

// ❌ 错误:同类方法调用,绕过代理
@Service
public class OrderService {
    
    public void methodA() {
        this.methodB();  // 直接调用,@DS不生效
    }
    
    @DS("order")
    public void methodB() {
        // ...
    }
}

// ✅ 正确:注入自身代理
@Service
public class OrderService {
    
    @Autowired
    @Lazy  // 避免循环依赖
    private OrderService self;
    
    public void methodA() {
        self.methodB();  // 通过代理调用,@DS生效
    }
    
    @DS("order")
    public void methodB() {
        // ...
    }
}

// ✅ 正确:使用AopContext
@Service
public class OrderService {
    
    public void methodA() {
        OrderService proxy = (OrderService) AopContext.currentProxy();
        proxy.methodB();
    }
    
    @DS("order")
    public void methodB() {
        // ...
    }
}

6.2 事务失效问题

问题描述

@DS@Transactional同时使用时,数据源切换失效或事务不生效。

问题场景还原

// ❌ 错误示例:事务导致数据源切换失效
@Service
public class OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    @DS("order")
    public void createOrder(Order order) {
        // 问题:@Transactional先于@DS执行
        // 事务管理器可能在默认数据源开启事务
        // 导致后续的@DS切换不生效
        orderMapper.insert(order);
    }
}

根因分析

Spring的事务管理器在事务开始时获取Connection,此时数据源尚未切换,导致事务绑定到错误的数据源。

正确使用姿势

// ✅ 方案一:确保注解顺序正确(@DS在@Transaction之前)
@Service
public class OrderService {
    
    @DS("order")  // 先切换数据源
    @Transactional(rollbackFor = Exception.class)  // 再开启事务
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
}

// ✅ 方案二:使用@DSTransactional(框架提供的组合注解)
@Service
public class OrderService {
    
    @DSTransactional  // 自动处理数据源切换和事务
    @DS("order")
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
}

// ✅ 方案三:类级别注解 + 方法级事务
@Service
@DS("order")  // 类级别指定数据源
public class OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
}

6.3 连接池配置陷阱

问题描述

HikariCP配置参数不生效或产生警告。

不支持的配置

# ❌ 错误:部分HikariCP属性在dynamic-datasource中不支持
spring:
  datasource:
    dynamic:
      datasource:
        master:
          hikari:
            pool-name: MyPool  # 不支持,会产生警告
            auto-commit: true  # 不支持
            read-only: false    # 不支持

支持的配置清单

# ✅ 正确:使用支持的配置参数
spring:
  datasource:
    dynamic:
      datasource:
        master:
          hikari:
            # 连接池大小配置
            minimum-idle: 5          # 最小空闲连接数
            maximum-pool-size: 20    # 最大连接数
            
            # 超时配置
            connection-timeout: 30000      # 连接超时(毫秒)
            idle-timeout: 600000           # 空闲超时(毫秒)
            max-lifetime: 1800000          # 最大存活时间(毫秒)
            
            # 连接验证
            connection-test-query: SELECT 1  # 连接测试查询
            
            # 泄漏检测
            leak-detection-threshold: 60000  # 泄漏检测阈值(毫秒)

连接泄漏检测

// 开启连接泄漏检测
spring:
  datasource:
    dynamic:
      datasource:
        master:
          hikari:
            leak-detection-threshold: 30000  # 30秒未归还连接视为泄漏

// 日志输出示例
// WARNING - Connection leak detection triggered for connection...

6.4 ThreadLocal线程安全

问题描述

在异步线程或线程池中,数据源切换异常或污染其他线程。

问题场景还原

// ❌ 错误:异步线程未清理数据源
@Service
public class AsyncService {
    
    @DS("order")
    public void processOrder(Order order) {
        // 在异步线程中执行
        CompletableFuture.runAsync(() -> {
            // 问题:ThreadLocal不会传递到异步线程
            // 数据源切换不生效
            orderMapper.updateById(order);
        });
    }
}

// ❌ 错误:线程池复用导致数据源污染
@Service
public class ThreadPoolService {
    
    @Autowired
    private ExecutorService executor;
    
    @DS("order")
    public void task1() {
        executor.submit(() -> {
            // 使用order数据源
            // 如果未清理,下一个任务可能错误使用order数据源
        });
    }
    
    public void task2() {
        // 可能错误地使用了task1遗留的数据源
        executor.submit(() -> {
            // ...
        });
    }
}

正确使用模式

// ✅ 正确:手动传递数据源上下文
@Service
public class AsyncService {
    
    @DS("order")
    public void processOrder(Order order) {
        // 获取当前数据源
        String currentDs = DynamicDataSourceContextHolder.peek();
        
        CompletableFuture.runAsync(() -> {
            try {
                // 在异步线程中设置数据源
                DynamicDataSourceContextHolder.push(currentDs);
                orderMapper.updateById(order);
            } finally {
                // 必须清理
                DynamicDataSourceContextHolder.poll();
            }
        });
    }
}

// ✅ 正确:使用装饰线程池
public class DataSourceAwareTaskDecorator implements TaskDecorator {
    
    @Override
    public Runnable decorate(Runnable runnable) {
        // 捕获当前线程的数据源
        String dsName = DynamicDataSourceContextHolder.peek();
        
        return () -> {
            try {
                // 设置到新线程
                if (dsName != null) {
                    DynamicDataSourceContextHolder.push(dsName);
                }
                runnable.run();
            } finally {
                // 清理
                DynamicDataSourceContextHolder.poll();
            }
        };
    }
}

// 配置线程池
@Configuration
public class AsyncConfig {
    
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        // 设置任务装饰器
        executor.setTaskDecorator(new DataSourceAwareTaskDecorator());
        return executor;
    }
}

6.5 性能优化要点

性能基准参考

以下为Dynamic-datasource框架在不同场景下的性能参考数据(基于4.3.0版本测试):

测试场景 测试条件 结果数据
数据源切换开销 单线程1000次切换 平均耗时 <0.1ms/次
@DS注解解析 AOP拦截10000次调用 平均耗时 <0.05ms/次
动态添加数据源 添加1个HikariCP数据源 初始化耗时 50-200ms
并发切换压力 100线程并发切换 吞吐量 >50000次/秒
连接获取开销 HikariCP连接池 连接获取 <1ms

性能影响因素

// 影响性能的关键因素及优化建议:

// 1. 数据源数量
// 建议:控制在10个以内,过多会增加路由决策开销
// 大量数据源场景考虑使用分组或动态加载

// 2. 连接池配置
// 禁忌:max-pool-size设置过大,导致资源浪费
// 建议:根据实际并发量设置,公式:(核心数*2) + 有效磁盘数

// 3. 事务边界
// 禁忌:事务范围过大,长时间占用连接
// 建议:事务最小化,避免在事务内执行远程调用、文件操作

// 4. SpEL表达式复杂度
// 禁忌:SpEL表达式过于复杂,每次调用都需要解析
// 建议:简单表达式优先,复杂逻辑可预计算后手动切换

连接池调优策略

spring:
  datasource:
    dynamic:
      datasource:
        master:
          hikari:
            # 根据业务并发量调整
            # 公式:maximum-pool-size = (核心数 * 2) + 有效磁盘数
            maximum-pool-size: 20
            
            # 最小空闲连接数,建议与maximum-pool-size相近或为其1/2
            minimum-idle: 10
            
            # 连接超时时间,不宜过长
            connection-timeout: 30000
            
            # 空闲连接超时,建议10分钟
            idle-timeout: 600000
            
            # 连接最大存活时间,建议30分钟
            max-lifetime: 1800000

数据源预热

@Component
@Slf4j
public class DataSourceWarmUp implements ApplicationRunner {
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    
    @Override
    public void run(ApplicationArguments args) {
        log.info("开始预热数据源连接池...");
        
        for (Map.Entry<String, DataSource> entry : dynamicRoutingDataSource.getDataSources().entrySet()) {
            String dsName = entry.getKey();
            DataSource ds = entry.getValue();
            
            if (ds instanceof HikariDataSource) {
                HikariDataSource hikari = (HikariDataSource) ds;
                // 获取最小连接数,触发连接池初始化
                int minIdle = hikari.getHikariConfigMXBean().getMinimumIdle();
                log.info("预热数据源 [{}],初始化 {} 个连接", dsName, minIdle);
            }
        }
        
        log.info("数据源预热完成");
    }
}

慢SQL监控配置

# MyBatis-Plus慢SQL监控
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      # 慢SQL阈值(毫秒)
      slow-query-threshold: 3000
// 自定义慢SQL拦截器
@Intercepts({
    @Signature(type = Executor.class, method = "query", 
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
@Slf4j
public class SlowSqlInterceptor implements Interceptor {
    
    private static final long SLOW_SQL_THRESHOLD = 3000; // 3秒
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        try {
            return invocation.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
            
            if (duration > SLOW_SQL_THRESHOLD) {
                MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
                log.warn("====== 慢SQL警告 ======");
                log.warn("SQL ID: {}", ms.getId());
                log.warn("执行时间: {} ms (阈值: {} ms)", duration, SLOW_SQL_THRESHOLD);
                log.warn("======================");
            }
        }
    }
}

七、最佳实践:工程化建议

7.1 使用规范

数据源命名规范

# 推荐的命名规范
spring:
  datasource:
    dynamic:
      primary: master  # 主数据源统一命名为master
      datasource:
        # 按业务域命名
        order:      # 订单域
        inventory:  # 库存域
        user:       # 用户域
        report:     # 报表域
        
        # 或按读写分离命名
        master:     # 主库(写)
        slave_1:    # 从库1(读)
        slave_2:    # 从库2(读)

Service层设计规范

// ✅ 推荐:按数据源拆分Service
@Service
@DS("order")
public class OrderQueryService {
    // 只包含订单库的查询操作
}

@Service
@DS("order")
public class OrderCommandService {
    // 只包含订单库的写入操作
}

// ❌ 不推荐:一个Service跨多个数据源
@Service
public class MixedService {
    
    @DS("order")
    public void orderOperation() {}
    
    @DS("inventory")
    public void inventoryOperation() {}
}

事务边界设计原则

// 原则1:事务最小化
// ✅ 正确:事务只包含必要的操作
@DS("order")
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
    orderMapper.insert(order);
    // 不包含远程调用、文件操作等非数据库操作
}

// ❌ 错误:事务包含过多操作
@DS("order")
@Transactional(rollbackFor = Exception.class)
public void createOrderWithNotify(Order order) {
    orderMapper.insert(order);
    // 远程调用不应在事务内
    notificationService.sendEmail(order);  // 可能导致事务长时间占用
}

// 原则2:明确事务传播行为
@DS("order")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
    orderMapper.insert(order);
}

// 原则3:只读事务优化查询性能
@DS("order")
@Transactional(readOnly = true)
public List<Order> listOrders() {
    return orderMapper.selectList(null);
}

7.2 监控与运维

数据源健康检查

@RestController
@RequestMapping("/actuator")
public class DataSourceHealthController {
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    
    @GetMapping("/datasource-health")
    public Map<String, Object> dataSourceHealth() {
        Map<String, Object> result = new HashMap<>();
        
        for (Map.Entry<String, DataSource> entry : dynamicRoutingDataSource.getDataSources().entrySet()) {
            String dsName = entry.getKey();
            DataSource ds = entry.getValue();
            
            Map<String, Object> dsInfo = new HashMap<>();
            
            if (ds instanceof HikariDataSource) {
                HikariDataSource hikari = (HikariDataSource) ds;
                HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
                
                dsInfo.put("activeConnections", pool.getActiveConnections());
                dsInfo.put("idleConnections", pool.getIdleConnections());
                dsInfo.put("totalConnections", pool.getTotalConnections());
                dsInfo.put("threadsAwaitingConnection", pool.getThreadsAwaitingConnection());
                
                // 健康状态判断
                boolean healthy = pool.getIdleConnections() > 0 
                    || pool.getActiveConnections() < hikari.getMaximumPoolSize();
                dsInfo.put("healthy", healthy);
            }
            
            result.put(dsName, dsInfo);
        }
        
        return result;
    }
}

连接池监控指标

@Component
@Slf4j
public class DataSourceMonitor {
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    
    @Scheduled(fixedRate = 60000)  // 每分钟执行
    public void monitorConnectionPool() {
        for (Map.Entry<String, DataSource> entry : dynamicRoutingDataSource.getDataSources().entrySet()) {
            String dsName = entry.getKey();
            DataSource ds = entry.getValue();
            
            if (ds instanceof HikariDataSource) {
                HikariDataSource hikari = (HikariDataSource) ds;
                HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
                
                log.info("数据源 [{}] 连接池状态: 活跃={}, 空闲={}, 总数={}, 等待={}",
                    dsName,
                    pool.getActiveConnections(),
                    pool.getIdleConnections(),
                    pool.getTotalConnections(),
                    pool.getThreadsAwaitingConnection());
                
                // 告警判断
                double usageRate = (double) pool.getActiveConnections() / hikari.getMaximumPoolSize();
                if (usageRate > 0.8) {
                    log.warn("数据源 [{}] 连接池使用率过高: {}%", dsName, String.format("%.2f", usageRate * 100));
                }
            }
        }
    }
}

日志与告警策略

# Logback配置示例
logging:
  level:
    com.baomidou.dynamic.datasource: DEBUG
    com.zaxxer.hikari: INFO
// 关键操作日志
@Aspect
@Component
@Slf4j
public class DataSourceLogAspect {
    
    @Around("@annotation(ds)")
    public Object logDataSourceSwitch(ProceedingJoinPoint point, DS ds) throws Throwable {
        String dsName = ds.value();
        String methodName = point.getSignature().getName();
        
        log.debug("方法 [{}] 切换到数据源 [{}]", methodName, dsName);
        
        long startTime = System.currentTimeMillis();
        try {
            Object result = point.proceed();
            long duration = System.currentTimeMillis() - startTime;
            log.debug("方法 [{}] 执行完成,耗时 {} ms", methodName, duration);
            return result;
        } catch (Exception e) {
            log.error("方法 [{}] 执行异常: {}", methodName, e.getMessage());
            throw e;
        }
    }
}

7.3 架构演进建议

多数据源 vs 微服务拆分选择

场景 推荐方案
业务耦合度高,但需要访问不同库 多数据源
业务相对独立,团队规模小 多数据源
业务独立演进,团队规模大 微服务拆分
需要独立部署、独立扩展 微服务拆分
跨数据源事务要求严格 多数据源 + Seata

长期演进路线

阶段一:单体应用 + 多数据源
  └── 快速迭代,业务验证

阶段二:模块化单体 + 多数据源
  └── 代码层面解耦,为拆分做准备

阶段三:微服务拆分
  └── 按业务域拆分,逐步迁移
  └── 多数据源 -> 每个服务独立数据源

阶段四:服务治理
  └── 服务间通信(Feign/Dubbo)
  └── 分布式事务(Seata/Saga)
  └── 链路追踪(SkyWalking/Zipkin)

八、总结

核心要点回顾

  1. 场景认知:多数据源框架适用于读写分离、多租户、系统整合等场景,但需权衡是否应选择微服务架构。

  2. 方案选型:Dynamic-datasource适合中小规模多数据源场景,轻量级、易集成;ShardingSphere适合大规模分库分表场景。

  3. 核心原理:理解ThreadLocal上下文隔离、AOP切面、动态路由三大机制,是正确使用框架的基础。

  4. 使用规范:遵循注解优先级、事务边界、命名规范等最佳实践,避免踩坑。

  5. 避坑指南:关注事务失效、线程安全、连接池配置等常见问题,做好监控和告警。

框架适用边界

适用场景 不适用场景
3-5个数据源以内 超大规模分库分表(建议ShardingSphere)
业务间有一定耦合 业务完全独立,需要独立部署
跨数据源事务要求不高 强一致性跨库事务(需配合Seata)
团队规模较小 多团队独立开发部署

学习资源推荐


本文从问题场景出发,系统介绍了Dynamic-datasource框架的核心原理与实践方法。

posted @ 2026-05-13 14:18  flycloudy  阅读(33)  评论(0)    收藏  举报