dynamic-datasource-spring-boot-starter -动态数据源详解

-------------------------------------------------------------------------

dynamic-datasource-spring-boot-starter 的作用

一、dynamic-datasource-spring-boot-starter 核心作用

dynamic-datasource-spring-boot-starter 是一款基于 Spring Boot 的动态数据源切换组件,核心目的是帮你在 Spring Boot 项目中无侵入、低成本地实现多数据源切换(比如读写分离、分库分表、多租户按库隔离等场景),无需手动编写复杂的数据源切换逻辑。
简单说:它把「多数据源配置、动态切换、负载均衡」等能力封装成 Starter,你只需少量配置就能实现数据源的灵活切换,不用改核心业务代码。

二、核心功能(新手易懂版)

1. 多数据源配置

支持配置多个数据源(主库、从库、不同业务库),只需在 application.yml/application.properties 中声明即可,无需手动创建 DataSource 实例。示例配置:
yaml
 
 
spring:
  datasource:
    dynamic:
      # 主数据源(默认数据源)
      primary: master
      # 多数据源列表
      datasource:
        master: # 主库
          url: jdbc:mysql://localhost:3306/db_master
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave1: # 从库1
          url: jdbc:mysql://localhost:3306/db_slave1
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave2: # 从库2
          url: jdbc:mysql://localhost:3306/db_slave2
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
 

2. 动态切换数据源

通过注解 / API 两种方式切换数据源,对业务代码无侵入:
  • 注解方式(最常用):在 Service 方法上加 @DS 注解指定数据源
    java
     
    运行
     
     
     
     
    @Service
    public class UserService {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        // 使用主库(默认)
        public void addUser(User user) {
            jdbcTemplate.update("INSERT INTO user(name) VALUES (?)", user.getName());
        }
    
        // 手动指定从库1
        @DS("slave1")
        public List<User> listUser() {
            return jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class));
        }
    
        // 从库负载均衡(组件自动轮询slave1/slave2)
        @DS("slave") // 需先配置slave为从库组
        public List<User> listUserBySlave() {
            return jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class));
        }
    }
    
     
     
  • API 方式:手动编码切换(适合复杂逻辑)
    java
     
    运行
     
     
     
     
    // 切换到slave2
    DynamicDataSourceContextHolder.push("slave2");
    // 执行业务逻辑
    listUser();
    // 移除当前数据源(避免污染)
    DynamicDataSourceContextHolder.poll();
    
     
     

3. 高级能力(适配信创 / 复杂场景)

  • 支持读写分离:主库写、从库读,组件可自动做负载均衡;
  • 支持分库分表:配合 ShardingSphere 实现更复杂的分库分表;
  • 支持国产数据库:适配达梦、人大金仓、高斯等信创数据库;
  • 支持动态新增 / 删除数据源:运行时可通过 API 新增数据源(适合多租户场景);
  • 事务支持:保证切换数据源后事务的正确性。

三、适用场景(信创 / 业务开发常用)

  1. 读写分离:主库(MySQL/Oracle)负责写操作,从库负责读操作,提升性能;
  2. 多业务库隔离:不同业务模块用不同数据库(比如用户库、订单库),通过注解切换;
  3. 多租户按库隔离:每个租户一个独立数据库,运行时根据租户 ID 切换数据源;
  4. 信创改造:部分业务迁到国产数据库(如达梦),部分保留原有库,通过动态数据源平滑过渡。

四、使用前置条件

  1. 项目基于 Spring Boot(2.x/3.x 均支持);
  2. 引入依赖(Maven 示例):
    xml
     
     
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>3.6.1</version> <!-- 推荐稳定版 -->
    </dependency>
    
     
     
  3. 配置数据源信息(如上 application.yml 示例)。

总结

  1. dynamic-datasource-spring-boot-starter 核心是简化多数据源切换,无需手动编写数据源切换逻辑,注解 / API 即可实现;
  2. 适配信创场景(国产数据库)、读写分离、多租户等主流业务需求,是 Spring Boot 项目多数据源的首选组件;
  3. 对业务代码无侵入,配置简单,学习成本低,适合快速落地多数据源架构。
dynamic-datasource-spring-boot-starter使用详解

dynamic-datasource-spring-boot-starter 完整使用详解

dynamic-datasource-spring-boot-starter(简称动态数据源组件)是由苞米豆(baomidou)开源的 Spring Boot 多数据源解决方案,能极低成本实现多数据源切换、读写分离、动态增删数据源等能力。以下是从基础配置到高级用法的全套实操指南,新手也能直接落地。

一、核心前置准备

1. 环境要求

  • Spring Boot:2.0.x ~ 3.x(3.x 需使用组件 3.6.0+ 版本)
  • JDK:8+(信创场景建议 JDK 8/11)
  • 依赖管理:Maven/Gradle

2. 引入依赖(Maven)

xml
 
 
<!-- 核心依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.6.1</version> <!-- 稳定版,适配Spring Boot 2/3 -->
</dependency>

<!-- 数据库驱动(根据实际数据库选择,信创库需对应驱动) -->
<!-- MySQL -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- 达梦(信创) -->
<dependency>
    <groupId>com.dameng</groupId>
    <artifactId>DmJdbcDriver18</artifactId>
    <version>8.1.2.47</version>
</dependency>
<!-- MyBatis-Plus(可选,无也可使用) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
 

二、基础用法:多数据源配置与切换

1. 配置文件(application.yml)

核心是在 spring.datasource.dynamic 下配置主数据源和从数据源,支持多类型数据库混合配置(如 MySQL + 达梦)。
yaml
 
 
spring:
  datasource:
    dynamic:
      # 1. 核心配置
      primary: master # 默认数据源(未指定时使用)
      strict: false # 非严格模式:找不到指定数据源时用默认库;true则抛异常
      datasource:
        # 2. 主库(MySQL)
        master:
          url: jdbc:mysql://127.0.0.1:3306/db_master?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          # 连接池配置(默认使用HikariCP,可自定义)
          hikari:
            maximum-pool-size: 20
            minimum-idle: 5
            connection-timeout: 30000
        # 3. 从库1(MySQL)
        slave1:
          url: jdbc:mysql://127.0.0.1:3306/db_slave1?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 4. 从库2(达梦,信创示例)
        slave2:
          url: jdbc:dm://192.168.1.100:5236/DB_SLAVE2?SYSDBA=1
          username: SYSDBA
          password: DAMENG123
          driver-class-name: dm.jdbc.driver.DmDriver
      # 5. 全局配置(可选,统一设置所有数据源的连接池)
      hikari:
        max-lifetime: 1800000
 

2. 注解切换数据源(@DS)

这是最常用的方式,通过 @DS 注解指定方法 / 类使用的数据源,优先级:方法注解 > 类注解 > 默认数据源。

(1)基础使用示例

java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;

@Service
// 类上注解:该类所有方法默认使用slave1(可省略,默认用master)
// @DS("slave1")
public class UserService {

    @Resource
    private UserMapper userMapper;

    // 1. 无注解:使用默认数据源(master),用于写操作
    public void addUser(User user) {
        userMapper.insert(user);
    }

    // 2. 方法注解:指定slave1,用于读操作
    @DS("slave1")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }

    // 3. 方法注解:指定slave2(达梦库)
    @DS("slave2")
    public List<User> listUserByDm() {
        return userMapper.selectList(null);
    }
}
 

(2)@DS 注解支持的取值

表格
 
取值类型示例说明
具体数据源名称 @DS("slave1") 直接指定配置文件中的数据源名称
分组名称 @DS("slave") 需先配置数据源分组,自动负载均衡(见下文)
default @DS("default") 使用默认数据源(等同于不写注解)

3. API 手动切换数据源

适合复杂业务逻辑(如根据参数动态切换),需手动管理数据源上下文,核心类:DynamicDataSourceContextHolder
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public void handleOrder(Long tenantId) {
        try {
            // 1. 根据租户ID动态切换数据源(假设租户1用slave1,租户2用slave2)
            String dsName = tenantId == 1 ? "slave1" : "slave2";
            DynamicDataSourceContextHolder.push(dsName);
            
            // 2. 执行业务逻辑(此时使用指定的数据源)
            queryOrderData();
        } finally {
            // 3. 必须移除上下文,避免污染后续请求
            DynamicDataSourceContextHolder.poll();
        }
    }

    private void queryOrderData() {
        // 数据库操作逻辑
    }
}
 

三、高级用法

1. 读写分离(数据源分组 + 负载均衡)

组件支持将多个从库分组,自动实现轮询负载均衡,只需在配置文件中给数据源命名加前缀,注解指定分组名即可。

(1)配置分组(修改 application.yml)

yaml
 
 
spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master: # 主库(写)
          url: jdbc:mysql://127.0.0.1:3306/db_master
          # 省略其他配置...
        slave_1: # 从库1(读),前缀slave_
          url: jdbc:mysql://127.0.0.1:3306/db_slave1
        slave_2: # 从库2(读),前缀slave_
          url: jdbc:mysql://127.0.0.1:3306/db_slave2
 

(2)使用分组名切换

java
 
运行
 
 
 
 
@Service
public class UserService {

    // @DS("slave") 匹配所有slave_前缀的数据源,组件自动轮询slave_1/slave_2
    @DS("slave")
    public List<User> listAllUser() {
        return userMapper.selectList(null);
    }
}
 

2. 动态增删数据源(运行时配置)

支持在项目启动后新增 / 删除数据源(适合多租户、动态扩容场景),核心类:DynamicRoutingDataSource

(1)动态新增数据源示例

java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;

@Component
public class DynamicDsManager {

    @Resource
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    @Resource
    private DataSourceCreator dataSourceCreator; // 组件自带的数据源创建器

    /**
     * 新增数据源
     * @param dsName 数据源名称(唯一)
     * @param url 数据库连接地址
     * @param username 用户名
     * @param password 密码
     * @param driverClassName 驱动类名
     */
    public void addDataSource(String dsName, String url, String username, String password, String driverClassName) {
        // 1. 构建数据源配置
        DataSourceProperty property = new DataSourceProperty();
        property.setUrl(url);
        property.setUsername(username);
        property.setPassword(password);
        property.setDriverClassName(driverClassName);
        
        // 2. 创建数据源(自动适配连接池)
        DataSource dataSource = dataSourceCreator.createDataSource(property);
        
        // 3. 添加到动态数据源管理器
        dynamicRoutingDataSource.addDataSource(dsName, dataSource);
    }

    /**
     * 删除数据源
     * @param dsName 数据源名称
     */
    public void removeDataSource(String dsName) {
        dynamicRoutingDataSource.removeDataSource(dsName);
    }
}
 

(2)使用新增的数据源

java
 
运行
 
 
 
 
@Service
public class TenantService {

    @Resource
    private DynamicDsManager dynamicDsManager;

    public void initTenantDs(Long tenantId) {
        // 1. 新增租户专属数据源
        String dsName = "tenant_" + tenantId;
        dynamicDsManager.addDataSource(
            dsName,
            "jdbc:mysql://127.0.0.1:3306/tenant_" + tenantId,
            "root",
            "123456",
            "com.mysql.cj.jdbc.Driver"
        );
        
        // 2. 切换到该数据源并初始化数据
        DynamicDataSourceContextHolder.push(dsName);
        initTenantData();
        DynamicDataSourceContextHolder.poll();
    }
}
 

3. 事务支持

动态数据源组件兼容 Spring 事务,需注意:一个事务内只能使用一个数据源(事务中切换数据源无效)。

(1)正常事务示例(单数据源)

java
 
运行
 
 
 
 
@Service
public class UserService {

    // 事务内使用master数据源(默认)
    @Transactional(rollbackFor = Exception.class)
    public void updateUser(User user) {
        userMapper.updateById(user);
        // 即使手动切换数据源,事务内也不会生效
        DynamicDataSourceContextHolder.push("slave1");
        userMapper.selectById(user.getId()); // 仍使用master
    }
}
 

(2)多数据源事务(分布式事务)

如果需要跨数据源事务,需整合 Seata 等分布式事务框架,核心步骤:
  1. 引入 Seata 依赖;
  2. 配置 Seata 事务组;
  3. 在方法上添加 @GlobalTransactional 注解。

四、常见问题与避坑指南

1. 数据源切换不生效?

  • 检查 @DS 注解是否加在 Service 方法 上(加在 Controller/Mapper 上无效,因为 AOP 切面是 Service 层);
  • 检查方法是否被同类内部调用(AOP 失效),需通过 Bean 调用;
  • 检查 strict: true 时,是否拼写错数据源名称(严格模式会抛异常)。

2. 连接池配置不生效?

  • 组件默认使用 HikariCP,如需切换成 Druid,需引入 Druid 依赖并配置 spring.datasource.dynamic.type=com.alibaba.druid.pool.DruidDataSource
  • 全局连接池配置需写在 spring.datasource.dynamic.hikari 下,单个数据源的连接池配置优先级更高。

3. 信创数据库适配?

  • 确保驱动类名正确(如达梦:dm.jdbc.driver.DmDriver,人大金仓:com.kingbase8.Driver);
  • 连接 URL 按国产数据库规范配置;
  • 建议使用组件 3.6.0+ 版本,对国产数据库兼容性更好。

五、核心配置项速查

表格
 
配置项默认值说明
spring.datasource.dynamic.primary master 默认数据源名称
spring.datasource.dynamic.strict false 严格模式:找不到数据源时是否抛异常
spring.datasource.dynamic.datasource - 多数据源配置列表
spring.datasource.dynamic.type HikariCP 全局数据源类型(Druid/ HikariCP)
spring.datasource.dynamic.hikari - 全局连接池配置(HikariCP)

总结

  1. dynamic-datasource-spring-boot-starter 核心是通过 @DS 注解 / API 实现多数据源切换,无侵入、配置简单;
  2. 基础场景(读写分离)只需配置多数据源 + @DS 注解,高级场景(动态增删数据源)可通过 DynamicRoutingDataSource 实现;
  3. 避坑关键:注解加在 Service 方法上、事务内不切换数据源、信创库需适配驱动和 URL。
DynamicDataSourceContextHolder peek push poll 等方法使用详解

DynamicDataSourceContextHolder 核心方法(peek/push/poll)使用详解

DynamicDataSourceContextHolder 是 dynamic-datasource-spring-boot-starter 中手动控制数据源上下文的核心工具类,底层基于 ThreadLocal<Deque<String>> 实现(线程内的栈结构),push/peek/poll 等方法对应栈的「入栈 / 查看栈顶 / 出栈」操作,专门解决手动动态切换数据源的场景(如多租户、复杂业务逻辑动态选库)。

一、核心设计原理

先理解底层结构:
  • 每个线程有独立的 ThreadLocal 存储区,避免多线程数据源上下文污染;
  • 存储结构是双端队列(Deque)(栈),支持「嵌套切换数据源」(如方法 A 切换到 slave1,方法 A 调用的方法 B 再切换到 slave2,执行完 B 后回到 slave1);
  • 核心方法对应栈的基础操作:
    表格
     
    方法名栈操作核心作用
    push 入栈 将指定数据源名称放入栈顶(切换数据源)
    peek 查看栈顶 获取当前生效的数据源名称(不修改栈)
    poll 出栈 移除栈顶的数据源名称(恢复上一层数据源)
    clear 清空栈 清空当前线程的所有数据源上下文
     

二、核心方法逐个拆解(附示例)

1. push (String dsName) - 切换 / 设置数据源

作用

将指定的数据源名称(如 "slave1"、"tenant_001")放入线程上下文的栈顶,后续数据库操作会使用该数据源。

用法要点

  • 必须传入配置文件中已定义的数据源名称(严格模式下传错会抛异常,非严格模式用默认库);
  • 支持嵌套调用(多次 push),栈会逐层存储数据源名称。

基础示例

java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public void handleOrder(Long tenantId) {
        // 1. 初始状态:栈为空,使用默认数据源(master)
        System.out.println("初始数据源:" + DynamicDataSourceContextHolder.peek()); // null → 用master
        
        // 2. push:切换到租户专属数据源(tenant_001)
        String tenantDsName = "tenant_" + tenantId;
        DynamicDataSourceContextHolder.push(tenantDsName);
        System.out.println("push后数据源:" + DynamicDataSourceContextHolder.peek()); // tenant_001
        
        // 3. 执行业务逻辑(此时使用tenant_001数据源)
        queryOrderData();
        
        // 4. 嵌套push:临时切换到slave1(用于查基础数据)
        DynamicDataSourceContextHolder.push("slave1");
        System.out.println("嵌套push后数据源:" + DynamicDataSourceContextHolder.peek()); // slave1
        queryBaseData();
        
        // 5. 必须出栈(poll),恢复到上一层数据源
        DynamicDataSourceContextHolder.poll();
        System.out.println("嵌套poll后数据源:" + DynamicDataSourceContextHolder.peek()); // tenant_001
        
        // 6. 最终出栈,恢复默认数据源
        DynamicDataSourceContextHolder.poll();
        System.out.println("最终poll后数据源:" + DynamicDataSourceContextHolder.peek()); // null → 用master
    }

    private void queryOrderData() { /* 租户库操作 */ }
    private void queryBaseData() { /* 从库1操作 */ }
}
 

2. peek () - 查看当前生效的数据源

作用

获取当前线程上下文栈顶的数据源名称(仅查看,不修改栈结构),返回值:
  • null:栈为空,使用默认数据源(配置的 primary 库,如 master);
  • 非 null:返回栈顶的数据源名称(当前生效的数据源)。

典型场景

  • 日志打印 / 监控:记录当前使用的数据源;
  • 校验:判断是否已切换到目标数据源;
  • 调试:排查数据源切换是否生效。

示例

java
 
运行
 
 
 
 
// 校验是否切换到了slave1
if (!"slave1".equals(DynamicDataSourceContextHolder.peek())) {
    throw new RuntimeException("数据源切换失败,当前生效的是:" + DynamicDataSourceContextHolder.peek());
}
// 执行业务逻辑
listUser();
 

3. poll () - 恢复上一层数据源

作用

移除栈顶的数据源名称(出栈),返回被移除的数据源名称;若栈为空,返回 null

用法要点

  • 必须和 push 成对使用(放在 finally 块中),否则会导致后续请求复用错误的数据源上下文;
  • 嵌套 push 时,poll 会逐层恢复(先进后出)。

规范写法(try-finally 确保 poll 执行)

java
 
运行
 
 
 
 
public void processData(String dsName) {
    try {
        // 切换数据源
        DynamicDataSourceContextHolder.push(dsName);
        // 核心业务逻辑
        doBusiness();
    } finally {
        // 无论是否异常,都要出栈,避免上下文污染
        DynamicDataSourceContextHolder.poll();
    }
}
 

4. clear () - 清空所有数据源上下文

作用

清空当前线程的整个数据源栈(所有 push 的数据源名称都被移除),直接恢复到默认数据源。

适用场景

  • 批量操作结束后快速重置上下文;
  • 异常场景下强制清空(避免栈堆积)。

示例

java
 
运行
 
 
 
 
public void batchProcess(List<String> dsNames) {
    try {
        for (String dsName : dsNames) {
            DynamicDataSourceContextHolder.push(dsName);
            processSingleData(dsName);
            DynamicDataSourceContextHolder.poll();
        }
    } catch (Exception e) {
        // 异常时清空所有上下文,避免残留
        DynamicDataSourceContextHolder.clear();
        throw new RuntimeException("批量处理失败", e);
    }
}
 

5. 扩展方法:getDataSourceLookupKey ()

作用

等价于 peek(),是更语义化的别名,返回当前生效的数据源名称(推荐新代码使用)。

示例

java
 
运行
 
 
 
 
String currentDs = DynamicDataSourceContextHolder.getDataSourceLookupKey();
System.out.println("当前使用的数据源:" + (currentDs == null ? "默认库(master)" : currentDs));
 

三、常见使用场景与最佳实践

场景 1:多租户按库隔离(最常用)

java
 
运行
 
 
 
 
@Service
public class TenantService {

    @Resource
    private TenantDsConfigMapper dsConfigMapper;

    public void handleTenantRequest(Long tenantId) {
        // 1. 根据租户ID查询数据源配置
        String dsName = dsConfigMapper.getDsNameByTenantId(tenantId);
        if (dsName == null) {
            throw new RuntimeException("租户" + tenantId + "无专属数据源");
        }
        
        // 2. 安全切换数据源(try-finally 必加)
        try {
            DynamicDataSourceContextHolder.push(dsName);
            // 3. 执行租户专属操作(查询/修改租户数据)
            queryTenantData(tenantId);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    private void queryTenantData(Long tenantId) { /* 租户库操作 */ }
}
 

场景 2:嵌套切换数据源(方法调用链)

java
 
运行
 
 
 
 
@Service
public class NestedDsService {

    // 外层方法:切换到slave1
    public void outerMethod() {
        try {
            DynamicDataSourceContextHolder.push("slave1");
            System.out.println("外层数据源:" + DynamicDataSourceContextHolder.peek()); // slave1
            innerMethod(); // 调用内层方法
            System.out.println("内层执行后数据源:" + DynamicDataSourceContextHolder.peek()); // slave1
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    // 内层方法:临时切换到slave2
    private void innerMethod() {
        try {
            DynamicDataSourceContextHolder.push("slave2");
            System.out.println("内层数据源:" + DynamicDataSourceContextHolder.peek()); // slave2
            // 内层业务逻辑
        } finally {
            DynamicDataSourceContextHolder.poll(); // 移除slave2,回到slave1
        }
    }
}
 

最佳实践总结

  1. 必须成对使用 push/poll:所有 push 操作都要放在 try 块,poll 放在 finally 块,避免异常导致上下文残留;
  2. 避免滥用 clear ():clear 会清空整个栈,嵌套场景下慎用(优先用 poll 逐层恢复);
  3. 日志打印 peek ():关键节点打印当前数据源名称,方便排查切换异常;
  4. 严格校验数据源名称:push 前校验数据源是否存在(可通过 DynamicRoutingDataSource 获取所有已配置数据源);
  5. 事务内不切换数据源:Spring 事务内切换数据源无效(事务绑定初始数据源),需在事务外切换。

四、常见问题与避坑

问题 1:数据源切换后不生效?

  • 原因:push 后未执行数据库操作,或被 Spring 事务覆盖(事务内切换无效);
  • 解决:在事务注解(@Transactional)外执行 push 操作。

问题 2:多线程场景下数据源串库?

  • 原因:ThreadLocal 是线程私有,若使用线程池,需确保任务执行完后 clear ();
  • 解决:线程池任务结束前调用 DynamicDataSourceContextHolder.clear()

问题 3:嵌套 push 后忘记对应 poll?

  • 现象:后续请求使用错误的数据源;
  • 解决:嵌套 push 几次,就必须 poll 几次,或最终用 clear () 兜底。

总结

  1. push 是「切换数据源」,poll 是「恢复数据源」,二者必须成对出现在 try-finally 中,避免上下文污染;
  2. peek 仅用于「查看当前数据源」,不修改栈结构,适合日志 / 校验场景;
  3. 嵌套切换数据源时,栈结构会自动维护层级,poll 会逐层恢复,无需手动记录层级。
详解如下 注解 DS DSTransactional Master Slave

dynamic-datasource 核心注解详解(@DS/@DSTransactional/@Master/@Slave)

在 dynamic-datasource-spring-boot-starter 中,@DS/@DSTransactional/@Master/@Slave 是管控数据源切换和事务的核心注解,其中 @Master/@Slave 是 @DS 的简化别名,@DSTransactional 是适配动态数据源的事务注解。以下是逐个拆解 + 使用示例 + 避坑指南,新手也能直接落地。

一、核心注解关系梳理

先明确注解的层级和关联,避免混淆:
 
生成失败,请重试
  • @DS:基础注解,万能指定任意数据源(核心);
  • @Master/@Slave@DS 的语法糖,简化读写分离场景;
  • @DSTransactional:替代 Spring 原生 @Transactional,适配动态数据源的事务。

二、@DS 注解(核心)

1. 核心作用

指定方法 / 类使用的数据源,是动态数据源切换的基础入口,优先级:方法注解 > 类注解 > 全局默认数据源

2. 注解属性

表格
 
属性名类型默认值说明
value String "" 数据源名称 / 分组名(如 "slave1"、"slave",空值则使用全局默认数据源)

3. 使用场景与示例

(1)基础用法:指定具体数据源

java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;

@Service
// 类级别注解:该类所有方法默认使用 slave1(可选,优先级低于方法注解)
// @DS("slave1")
public class UserService {

    // 无注解:使用全局默认数据源(配置的 primary: master)
    public void addUser(User user) {
        userMapper.insert(user); // 走 master 库(写操作)
    }

    // 方法级别注解:强制指定 slave2 库
    @DS("slave2")
    public User getUserById(Long id) {
        return userMapper.selectById(id); // 走 slave2 库(读操作)
    }
}
 

(2)进阶用法:指定数据源分组(负载均衡)

先在配置文件中给数据源命名加统一前缀(如 slave_1slave_2),然后 @DS("slave") 会自动匹配所有前缀为 slave_ 的数据源,实现轮询负载均衡:
java
 
运行
 
 
 
 
// @DS("slave") 匹配 slave_1、slave_2,组件自动轮询
@DS("slave")
public List<User> listAllUser() {
    return userMapper.selectList(null);
}
 

(3)特殊值:@DS ("default")

等价于不写 @DS 注解,强制使用全局默认数据源(master):
java
 
运行
 
 
 
 
// 即使类注解指定了 slave1,方法注解 @DS("default") 会覆盖,走 master
@DS("default")
public void updateUser(User user) {
    userMapper.updateById(user);
}
 

4. 避坑要点

  • ❌ 不要加在 Controller/Mapper 层:@DS 基于 Spring AOP 实现,切面默认织入 Service 层,加在 Controller/Mapper 上会失效;
  • ❌ 不要同类内部调用:AOP 失效(如方法 A 调用同类方法 B,B 的 @DS 注解不生效),需通过 Bean 调用;
  • ✅ 支持继承:子类会继承父类的 @DS 注解,子类方法可覆盖。

三、@Master 注解(@DS ("master") 别名)

1. 核心作用

简化「指定主库」的写法,等价于 @DS("master"),专门用于写操作场景(插入 / 更新 / 删除)。

2. 注解源码(简化版)

java
 
运行
 
 
 
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DS("master") // 本质是@DS的别名
public @interface Master {
}
 

3. 使用示例

java
 
运行
 
 
 
 
@Service
public class OrderService {

    // 等价于 @DS("master"),强制走主库(写操作)
    @Master
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }

    // 类注解:该类所有方法默认走主库
    // @Master
    // public class OrderService { ... }
}
 

4. 适用场景

  • 读写分离架构中,明确指定方法走主库(避免误走从库导致写操作失败);
  • 简化代码,无需手写 @DS("master"),语义更清晰。

四、@Slave 注解(@DS ("slave") 别名)

1. 核心作用

简化「指定从库分组」的写法,等价于 @DS("slave"),专门用于读操作场景(查询),自动负载均衡所有 slave_ 前缀的从库。

2. 注解源码(简化版)

java
 
运行
 
 
 
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DS("slave") // 本质是@DS的别名
public @interface Slave {
}
 

3. 使用前提

配置文件中需有 slave_ 前缀的从库(如 slave_1slave_2):
yaml
 
 
spring:
  datasource:
    dynamic:
      datasource:
        master: # 主库
          url: jdbc:mysql://127.0.0.1:3306/db_master
        slave_1: # 从库1(前缀 slave_)
          url: jdbc:mysql://127.0.0.1:3306/db_slave1
        slave_2: # 从库2(前缀 slave_)
          url: jdbc:mysql://127.0.0.1:3306/db_slave2
 

4. 使用示例

java
 
运行
 
 
 
 
@Service
public class OrderService {

    // 等价于 @DS("slave"),自动轮询 slave_1、slave_2(读操作)
    @Slave
    public List<Order> listOrderByUserId(Long userId) {
        return orderMapper.selectByUserId(userId);
    }
}
 

5. 避坑要点

  • ❌ 不要在 @Slave 注解的方法中执行写操作:从库通常是只读的,写操作会抛异常;
  • ✅ 若只有单个从库(如 slave1,无下划线),需用 @DS("slave1")@Slave 会失效(匹配不到 slave_ 前缀)。

五、@DSTransactional 注解(动态数据源事务)

1. 核心作用

替代 Spring 原生 @Transactional,解决「动态数据源切换后事务不生效」的问题,确保事务绑定当前切换后的数据源。

2. 与 @Transactional 的区别

表格
 
特性@Transactional(原生)@DSTransactional(动态数据源)
数据源绑定 绑定事务开始时的默认数据源 绑定 @DS 指定的动态数据源
多数据源事务 不支持(仅绑定初始数据源) 支持(绑定当前 @DS 指定的数据源)
注解属性 全量 Spring 事务属性 完全兼容 @Transactional 的所有属性

3. 注解属性(与 @Transactional 一致)

核心属性:
表格
 
属性名说明
rollbackFor 指定触发回滚的异常类型(如 rollbackFor = Exception.class
propagation 事务传播行为(如 Propagation.REQUIRED
readOnly 是否只读事务(读操作建议设为 true)

4. 使用场景与示例

(1)基础用法:单数据源事务

java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // 1. 指定数据源为 slave1
    @DS("slave1")
    // 2. 动态数据源事务:绑定 slave1 库,异常时回滚
    @DSTransactional(rollbackFor = Exception.class)
    public void batchUpdateUserStatus(List<Long> userIds, Integer status) {
        for (Long userId : userIds) {
            userMapper.updateStatus(userId, status);
            // 模拟异常:触发回滚
            if (userId == 100L) {
                throw new RuntimeException("更新失败,触发回滚");
            }
        }
    }
}
 

(2)读写分离 + 事务(主库写)

java
 
运行
 
 
 
 
@Service
public class OrderService {

    // 强制走主库 + 事务(写操作必须用主库)
    @Master
    @DSTransactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 1. 插入订单
        orderMapper.insert(order);
        // 2. 更新用户订单数
        userMapper.incrOrderCount(order.getUserId());
        // 异常时,主库的两个操作都回滚
    }
}
 

5. 避坑要点

  • ❌ 不要在事务内切换数据源:@DSTransactional 绑定事务开始时的数据源,事务内 push/@DS 切换数据源无效;
  • ✅ 事务注解需和 @DS/@Master/@Slave 注解在同一个方法上;
  • ✅ 分布式事务:若需跨数据源事务,需整合 Seata,@DSTransactional 替换为 @GlobalTransactional
  • ❌ 不要混用 @Transactional 和 @DSTransactional:同一方法同时加两个注解会导致事务异常。

六、注解使用优先级与最佳实践

1. 优先级排序(从高到低)

  1. 方法上的 @DS > 方法上的 @Master/@Slave
  2. 类上的 @DS > 类上的 @Master/@Slave
  3. 全局默认数据源(配置的 primary: master)。
示例:
java
 
运行
 
 
 
 
@Service
@Slave // 类注解:默认走从库
public class UserService {

    // 方法注解 @Master 覆盖类注解 @Slave,强制走主库
    @Master
    public void updateUser(User user) {
        userMapper.updateById(user);
    }

    // 无方法注解,继承类注解 @Slave,走从库
    public List<User> listUser() {
        return userMapper.selectList(null);
    }
}
 

2. 最佳实践总结

表格
 
场景推荐注解组合
主库写操作 @Master + @DSTransactional
从库读操作(负载均衡) @Slave
指定单个从库读操作 @DS("slave1")
动态切换租户库 @DS("#{#tenantId}")(SpEL 表达式)+ @DSTransactional

SpEL 表达式进阶用法(动态传参)

@DS 支持 SpEL 表达式,可直接引用方法参数,实现「参数化切换数据源」:
java
 
运行
 
 
 
 
// 根据方法参数 tenantId 动态切换数据源(如 tenant_001、tenant_002)
@DS("#{#tenantId}")
public List<User> listTenantUser(String tenantId) {
    return userMapper.selectTenantUser(tenantId);
}
 

总结

  1. @DS 是核心,可指定任意数据源 / 分组,@Master/@Slave 是其简化别名(适配读写分离);
  2. @Master 等价于 @DS("master")(主库写),@Slave 等价于 @DS("slave")(从库读,需配置 slave_ 前缀);
  3. @DSTransactional 替代原生 @Transactional,确保动态数据源的事务生效,事务内不可切换数据源。
分布式事务:若需跨数据源事务,需整合 Seata,@DSTransactional 替换为 @GlobalTransactional;代码详解

动态数据源 + Seata 分布式事务(@GlobalTransactional)代码详解

当使用 dynamic-datasource-spring-boot-starter 实现跨多数据源事务(如主库 + 从库、多租户库、不同业务库)时,单库的 @DSTransactional 无法满足需求,需整合 Seata(阿里开源分布式事务框架),并将 @DSTransactional 替换为 Seata 的 @GlobalTransactional。以下是完整可落地的代码示例 + 配置步骤,涵盖 Spring Boot 2.x/3.x 适配。

一、核心原理

 
生成失败,请重试
  • TM(事务管理器):发起全局事务,生成 XID(全局事务 ID);
  • RM(资源管理器):各数据源的分支事务,注册到 Seata TC;
  • TC(事务协调器):协调所有分支事务,统一提交 / 回滚;
  • XID 透传:Seata 自动将 XID 绑定到线程上下文,动态数据源切换时仍能传递,保证分支事务归属同一全局事务。

二、前置准备

1. 环境依赖

表格
 
组件版本要求
Spring Boot 2.3.x ~ 3.2.x
dynamic-datasource 3.6.0+
Seata 1.5.2+(适配 Spring Boot 3)
MySQL 5.7+ / 8.0+(或国产数据库)
Seata Server 1.5.2+(需单独部署)

2. 部署 Seata Server

  • 下载 Seata Server:Seata 官网
  • 配置 registry.conf(注册中心,推荐 Nacos)和 file.conf(事务日志存储,推荐 DB);
  • 启动 Seata Server:sh bin/seata-server.sh -p 8091 -h 127.0.0.1

三、项目配置

1. 引入依赖(Maven)

xml
 
 
<!-- 动态数据源核心 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.6.1</version>
</dependency>

<!-- Seata 核心依赖 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>

<!-- Seata 数据源代理(适配动态数据源) -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-datasource-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>

<!-- MySQL 驱动(根据实际数据库替换) -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- MyBatis-Plus(可选,简化CRUD) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
 

2. 配置文件(application.yml)

yaml
 
 
spring:
  # 动态数据源配置
  datasource:
    dynamic:
      primary: master # 默认数据源
      strict: false # 非严格模式
      datasource:
        # 数据源1:主库(订单库)
        master:
          url: jdbc:mysql://127.0.0.1:3306/db_order?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          # Seata 代理数据源(关键配置)
          type: com.zaxxer.hikari.HikariDataSource
        # 数据源2:从库/业务库(用户库)
        slave1:
          url: jdbc:mysql://127.0.0.1:3306/db_user?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.zaxxer.hikari.HikariDataSource

# Seata 配置(核心)
seata:
  enabled: true
  # 事务组配置(需与 Seata Server 的 service.vgroupMapping 一致)
  tx-service-group: my_test_tx_group
  # 关闭自动代理(动态数据源手动代理)
  enable-auto-data-source-proxy: false
  # 服务配置
  service:
    vgroup-mapping:
      my_test_tx_group: default # 事务组 -> 集群名
    grouplist:
      default: 127.0.0.1:8091 # Seata Server 地址
  # 配置中心(默认file,可改为nacos)
  config:
    type: file
  # 注册中心(默认file,可改为nacos)
  registry:
    type: file
  # 数据源代理模式(AT模式,主流)
  data-source-proxy-mode: AT
 

3. 动态数据源 + Seata 代理配置(关键)

需手动配置 Seata 数据源代理,让动态数据源的每个库都被 Seata 代理,否则分布式事务不生效:
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DynamicDataSourceSeataConfig {

    private final DynamicDataSourceProperties properties;

    public DynamicDataSourceSeataConfig(DynamicDataSourceProperties properties) {
        this.properties = properties;
    }

    /**
     * 配置动态数据源,并为每个数据源添加 Seata 代理
     */
    @Bean
    @Primary
    public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
        // 1. 创建动态数据源
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
        dynamicRoutingDataSource.setPrimary(properties.getPrimary());
        dynamicRoutingDataSource.setStrict(properties.getStrict());
        dynamicRoutingDataSource.setDataSourceProvider(dynamicDataSourceProvider);
        dynamicRoutingDataSource.setStrategy(properties.getStrategy());
        dynamicRoutingDataSource.setP6spy(properties.getP6spy());
        dynamicRoutingDataSource.setSeata(properties.getSeata());

        // 2. 初始化动态数据源
        dynamicRoutingDataSource.afterPropertiesSet();

        // 3. 为动态数据源添加 Seata 代理(核心)
        return new DataSourceProxy(dynamicRoutingDataSource);
    }
}
 

四、核心代码实现

1. 实体类 & Mapper(示例)

(1)订单实体(db_order 库)

java
 
运行
 
 
 
 
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("t_order")
public class Order {
    private Long id;
    private Long userId;
    private String orderNo;
    private Integer amount;
}
 

(2)用户实体(db_user 库)

java
 
运行
 
 
 
 
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("t_user")
public class User {
    private Long id;
    private String username;
    private Integer balance;
}
 

(3)Mapper 接口

java
 
运行
 
 
 
 
// 订单 Mapper(默认走 master 库)
public interface OrderMapper extends BaseMapper<Order> {
}

// 用户 Mapper
public interface UserMapper extends BaseMapper<User> {
    // 扣减余额
    @Update("UPDATE t_user SET balance = balance - #{amount} WHERE id = #{userId}")
    int deductBalance(@Param("userId") Long userId, @Param("amount") Integer amount);
}
 

2. Service 层(核心:@GlobalTransactional)

替换 @DSTransactional 为 @GlobalTransactional,实现跨 master(订单库)和 slave1(用户库)的分布式事务:
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.annotation.DS;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class OrderUserService {

    @Resource
    private OrderMapper orderMapper;
    @Resource
    private UserMapper userMapper;

    /**
     * 跨数据源分布式事务:创建订单 + 扣减用户余额
     * @GlobalTransactional 替代 @DSTransactional,实现全局事务
     * rollbackFor = Exception.class:所有异常都回滚
     * timeoutMills:全局事务超时时间(默认60秒)
     */
    @GlobalTransactional(rollbackFor = Exception.class, timeoutMills = 60000)
    public void createOrderAndDeductBalance(Long userId, Integer amount) {
        // 打印 XID(全局事务ID),方便排查问题
        System.out.println("全局事务XID:" + RootContext.getXID());

        try {
            // ========== 第一步:切换到 master 库(订单库),创建订单 ==========
            // @DS("master") 可省略(默认主库),此处显式指定更清晰
            @DS("master")
            class OrderOperation {}
            Order order = new Order();
            order.setUserId(userId);
            order.setOrderNo("ORDER_" + System.currentTimeMillis());
            order.setAmount(amount);
            orderMapper.insert(order);
            System.out.println("订单创建成功,ID:" + order.getId());

            // ========== 第二步:切换到 slave1 库(用户库),扣减余额 ==========
            DynamicDataSourceContextHolder.push("slave1");
            int affectRows = userMapper.deductBalance(userId, amount);
            if (affectRows == 0) {
                throw new RuntimeException("用户余额扣减失败,用户ID:" + userId);
            }
            System.out.println("用户余额扣减成功,用户ID:" + userId);

            // 模拟异常:触发全局事务回滚(注释掉则正常提交)
            // throw new RuntimeException("模拟业务异常,触发全局回滚");
        } finally {
            // 恢复数据源上下文
            DynamicDataSourceContextHolder.poll();
        }
    }
}
 

3. Controller 层(测试入口)

java
 
运行
 
 
 
 
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class OrderController {

    @Resource
    private OrderUserService orderUserService;

    /**
     * 测试接口:http://localhost:8080/order/create/1/100
     * @param userId 用户ID
     * @param amount 订单金额/扣减余额
     */
    @GetMapping("/order/create/{userId}/{amount}")
    public String createOrder(@PathVariable Long userId, @PathVariable Integer amount) {
        try {
            orderUserService.createOrderAndDeductBalance(userId, amount);
            return "订单创建+余额扣减成功!";
        } catch (Exception e) {
            return "失败:" + e.getMessage();
        }
    }
}
 

五、关键说明 & 避坑指南

1. @GlobalTransactional 核心属性

表格
 
属性名作用
rollbackFor 指定触发回滚的异常类型(必配,否则 RuntimeException 以外的异常不回滚)
timeoutMills 全局事务超时时间(默认 60000ms,建议根据业务调整)
name 全局事务名称(可选,用于 Seata 控制台识别)
propagation 事务传播行为(默认 REQUIRED,无需修改)

2. 避坑要点

(1)Seata 数据源代理必须包裹动态数据源

  • ❌ 错误:仅代理单个数据源;
  • ✅ 正确:代理 DynamicRoutingDataSource(如上述配置类),确保所有动态数据源都被 Seata 代理。

(2)XID 必须透传

  • Seata 基于 ThreadLocal 传递 XID,动态数据源切换(push/poll)不会丢失 XID,但线程池场景需手动传递:
    java
     
    运行
     
     
     
     
    // 线程池任务中传递 XID
    String xid = RootContext.getXID();
    executorService.submit(() -> {
        try {
            RootContext.bind(xid); // 绑定XID
            // 执行业务逻辑
        } finally {
            RootContext.unbind(); // 解绑
        }
    });
    
     
     

(3)事务内切换数据源的正确方式

  • @DS 注解或 DynamicDataSourceContextHolder.push() 都可,但必须在 @GlobalTransactional 内执行;
  • 事务内切换数据源后,分支事务会自动注册到全局事务。

(4)国产数据库适配

  • 达梦 / 人大金仓等国产数据库需确保 Seata 支持(Seata 1.5+ 已适配);
  • 驱动类名和 URL 按国产数据库规范配置,代理方式不变。

3. 验证分布式事务

(1)正常提交场景

  • 调用接口:http://localhost:8080/order/create/1/100
  • 结果:db_order.t_order 新增订单,db_user.t_user 余额扣减,Seata 控制台显示事务状态为 committed

(2)异常回滚场景

  • 打开代码中「模拟异常」的注释;
  • 调用接口后,订单库和用户库的数据都回滚(无新增、无扣减),Seata 控制台显示事务状态为 rolled back

六、Seata 控制台监控

启动 Seata Server 后,访问 http://localhost:7091(Seata 控制台),可查看:
  • 全局事务列表(XID、状态、耗时);
  • 分支事务列表(各数据源的分支事务);
  • 事务提交 / 回滚日志。

总结

  1. 跨动态数据源的分布式事务需整合 Seata,核心是将 @DSTransactional 替换为 @GlobalTransactional
  2. 关键配置:Seata 代理动态数据源(DataSourceProxy 包裹 DynamicRoutingDataSource),确保所有库都被代理;
  3. 核心要点:XID 透传、push/poll 成对使用、异常时全局回滚所有数据源的操作;
  4. 验证方式:通过模拟异常,确认多数据源的数据是否同时回滚,确保分布式事务一致性。

-------------------------------------------------------------------------

DynamicDataSourceStrategy 及子类(负载均衡 / 随机策略)使用详解

 
DynamicDataSourceStrategydynamic-datasource-spring-boot-starter 中数据源负载均衡策略的核心接口,LoadBalanceDynamicDataSourceStrategy(轮询)和 RandomDynamicDataSourceStrategy(随机)是其内置实现类,专门解决「多从库分组负载均衡」场景(如 @Slave 注解匹配多个从库时,自动选择其中一个执行)。以下是配置方式 + 代码示例 + 场景适配,新手也能直接落地。
 

一、核心概念梳理

 

1. 策略接口与实现类

 
表格
 
类名核心作用适用场景
DynamicDataSourceStrategy 负载均衡策略接口,定义「从数据源列表中选一个」的规则 自定义策略时实现该接口
LoadBalanceDynamicDataSourceStrategy 内置轮询策略(默认):按顺序循环选择数据源 从库性能相近,需均匀分配请求
RandomDynamicDataSourceStrategy 内置随机策略:随机选择一个数据源 从库性能差异大,需分散压力
 

2. 核心使用场景

 
只有当你配置了数据源分组(如 slave_1slave_2 前缀为 slave_ 的从库),并通过 @DS("slave")/@Slave 引用分组名时,策略才会生效 —— 组件会先筛选出该分组下的所有数据源,再通过指定策略选择一个执行。
 

二、快速使用(配置文件方式)

 

1. 基础配置(默认轮询策略)

 

步骤 1:配置多从库分组

 
application.yml 中配置 slave_ 前缀的从库(分组名是 slave):
 
yaml
 
 
spring:
  datasource:
    dynamic:
      primary: master # 默认主库
      strict: false
      # 全局负载均衡策略(可选,默认轮询)
      strategy: com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy
      datasource:
        # 主库
        master:
          url: jdbc:mysql://127.0.0.1:3306/db_master
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 从库1(前缀 slave_)
        slave_1:
          url: jdbc:mysql://127.0.0.1:3306/db_slave1
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 从库2(前缀 slave_)
        slave_2:
          url: jdbc:mysql://127.0.0.1:3306/db_slave2
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 从库3(前缀 slave_)
        slave_3:
          url: jdbc:mysql://127.0.0.1:3306/db_slave3
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
 
 

步骤 2:使用分组名触发负载均衡

 
在 Service 层用 @Slave(等价于 @DS("slave")),组件会自动按轮询策略选择 slave_1/slave_2/slave_3
 
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.annotation.Slave;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // @Slave 等价于 @DS("slave"),触发分组负载均衡
    @Slave
    public List<User> listAllUser() {
        // 第一次调用:slave_1
        // 第二次调用:slave_2
        // 第三次调用:slave_3
        // 第四次调用:slave_1(轮询重置)
        return userMapper.selectList(null);
    }
}
 
 

2. 切换为随机策略(两种方式)

 

方式 1:配置文件指定(全局生效)

 
修改 application.yml 中的 strategy 配置:
 
yaml
 
 
spring:
  datasource:
    dynamic:
      # 全局切换为随机策略
      strategy: com.baomidou.dynamic.datasource.strategy.RandomDynamicDataSourceStrategy
      # 其他配置不变...
 
 

方式 2:自定义配置类(全局生效)

 
通过 @Bean 注入策略,优先级高于配置文件:
 
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.strategy.RandomDynamicDataSourceStrategy;
import com.baomidou.dynamic.datasource.strategy.DynamicDataSourceStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DynamicDataSourceConfig {

    /**
     * 注入随机负载均衡策略(全局生效)
     */
    @Bean
    public DynamicDataSourceStrategy dynamicDataSourceStrategy() {
        return new RandomDynamicDataSourceStrategy();
    }
}
 
 
此时 @Slave 注解的方法会随机选择 slave_1/slave_2/slave_3,每次调用的数据源不固定。
 

三、进阶使用(自定义策略)

 
若内置的轮询 / 随机策略不满足需求(如按从库负载、响应时间选择),可实现 DynamicDataSourceStrategy 接口自定义策略。
 

1. 自定义策略示例(按数据源名称匹配优先级)

 
需求:优先选择 slave_1,若不可用则选 slave_2,最后选 slave_3
 
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.strategy.DynamicDataSourceStrategy;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class PriorityDynamicDataSourceStrategy implements DynamicDataSourceStrategy {

    // 记录slave_1是否可用(示例,实际可通过健康检查判断)
    private volatile boolean slave1Available = true;

    @Override
    public String determineDataSource(List<String> dataSources) {
        // dataSources:分组下的所有数据源名称(如 [slave_1, slave_2, slave_3])
        if (dataSources.isEmpty()) {
            throw new RuntimeException("分组下无可用数据源");
        }

        // 优先级1:slave_1可用则选它
        if (slave1Available && dataSources.contains("slave_1")) {
            return "slave_1";
        }
        // 优先级2:选slave_2
        else if (dataSources.contains("slave_2")) {
            return "slave_2";
        }
        // 优先级3:选第一个数据源(slave_3)
        else {
            return dataSources.get(0);
        }
    }

    // 手动切换slave_1可用性(示例)
    public void setSlave1Available(boolean available) {
        this.slave1Available = available;
    }
}
 
 

2. 启用自定义策略

 
java
 
运行
 
 
 
 
@Configuration
public class DynamicDataSourceConfig {

    // 注入自定义策略(覆盖内置策略)
    @Bean
    public DynamicDataSourceStrategy dynamicDataSourceStrategy() {
        return new PriorityDynamicDataSourceStrategy();
    }
}
 
 

四、关键细节与避坑指南

 

1. 策略生效条件

 
  • 必须配置分组数据源(名称带下划线前缀,如 slave_1),且通过 @DS("分组名")(如 @DS("slave"))引用;
  • 若分组下只有一个数据源,策略不会生效(直接选该数据源);
  • 策略仅作用于「读操作」(从库),主库(@Master/@DS("master"))无负载均衡(只有一个主库)。
 

2. 内置策略核心逻辑(源码简化版)

 

(1)轮询策略(LoadBalanceDynamicDataSourceStrategy)

 
java
 
运行
 
 
 
 
public class LoadBalanceDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
    private final AtomicInteger index = new AtomicInteger(0);

    @Override
    public String determineDataSource(List<String> dataSources) {
        // 超出列表长度则重置为0,循环选择
        int i = index.getAndIncrement() % dataSources.size();
        if (i < 0) {
            i = 0;
            index.set(0);
        }
        return dataSources.get(i);
    }
}
 
 

(2)随机策略(RandomDynamicDataSourceStrategy)

 
java
 
运行
 
 
 
 
public class RandomDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
    private final Random random = new Random();

    @Override
    public String determineDataSource(List<String> dataSources) {
        // 随机选一个索引
        int index = random.nextInt(dataSources.size());
        return dataSources.get(index);
    }
}
 
 

3. 常见问题

 

问题 1:策略不生效?

 
  • 排查:是否配置了下划线前缀的分组数据源?是否用 @DS("分组名") 而非具体数据源名(如 @DS("slave_1"))?
  • 解决:确保分组名和数据源前缀一致(如 slave 分组对应 slave_ 前缀)。
 

问题 2:轮询策略出现重复选择?

 
  • 原因:AtomicInteger 是全局的,若分组下有多个不同前缀的分组(如 slave_read_),会共用同一个计数器;
  • 解决:自定义策略,为不同分组维护独立计数器。
 

问题 3:随机策略导致某从库压力过大?

 
  • 原因:纯随机无权重,可能导致请求集中;
  • 解决:自定义加权随机策略(按从库性能分配权重)。
 

五、适用场景总结

 
表格
 
策略类核心特点适用场景
LoadBalanceDynamicDataSourceStrategy(轮询) 请求均匀分配,无热点从库 从库配置相同、性能相近
RandomDynamicDataSourceStrategy(随机) 分散压力,避免固定顺序 从库性能差异大、需打散请求
自定义策略 按需定制规则 有特殊优先级、健康检查、权重需求
 

总结

 
  1. DynamicDataSourceStrategy 是负载均衡策略接口,内置 轮询(默认)随机 两种实现;
  2. 使用核心:配置下划线前缀的分组数据源 + @DS("分组名"),策略自动生效;
  3. 全局切换策略可通过「配置文件指定类名」或「配置类注入 Bean」实现,自定义策略需实现接口并注入;
  4. 策略仅作用于多从库分组,主库无负载均衡,单从库分组也不会触发策略。

-------------------------------------------------------------------------

dynamic-datasource-spring-boot-starter 全维度使用详解

 
dynamic-datasource-spring-boot-starter(简称动态数据源组件)是苞米豆(baomidou)开源的 Spring Boot 多数据源解决方案,核心价值是无侵入、低成本实现多数据源切换,支持读写分离、多租户、动态增删数据源等主流场景。以下从「核心原理→基础配置→高级用法→避坑指南」全流程讲解,新手也能直接落地。
 

一、核心原理

 

1. 整体架构

 
 

配置多数据源

DynamicRoutingDataSource(核心路由类)

生成失败,请重试
 

配置多数据源

DynamicRoutingDataSource(核心路由类)

生成失败,请重试
豆包
你的 AI 助手,助力每日工作学习
 
  • 核心类:DynamicRoutingDataSource 实现 Spring DataSource 接口,负责路由到目标数据源;
  • 上下文管理:DynamicDataSourceContextHolder 基于 ThreadLocal 存储当前线程的数据源名称,避免多线程污染;
  • 切换触发:@DS 注解(AOP 拦截)或 API 手动切换;
  • 负载均衡:DynamicDataSourceStrategy 实现多从库的轮询 / 随机 / 自定义负载均衡。
 

2. 核心特性

 
  • 无侵入:无需修改业务代码,注解 / API 即可切换;
  • 多数据源支持:配置文件 / 动态新增多数据源;
  • 负载均衡:内置轮询 / 随机策略,支持自定义;
  • 事务兼容:适配 @DSTransactional/Seata 分布式事务;
  • 国产数据库适配:支持达梦、人大金仓、高斯等信创库。
 

二、快速入门(基础配置)

 

1. 环境依赖

 
表格
 
组件版本要求
Spring Boot 2.0.x ~ 3.x(3.x 需 3.6.0+)
JDK 8+
数据库驱动 对应使用的数据库(MySQL / 达梦等)
 

2. 引入依赖(Maven)

 
xml
 
 
<!-- 动态数据源核心依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.6.1</version> <!-- 稳定版 -->
</dependency>

<!-- 数据库驱动(示例:MySQL) -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- MyBatis-Plus(可选,简化CRUD) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
 
 

3. 配置多数据源(application.yml)

 
yaml
 
 
spring:
  datasource:
    dynamic:
      # 1. 核心配置
      primary: master # 默认数据源(未指定时使用)
      strict: false # 非严格模式:找不到数据源时用默认库;true则抛异常
      strategy: com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy # 全局负载均衡策略(默认轮询)
      # 2. 多数据源列表
      datasource:
        # 主库(写)
        master:
          url: jdbc:mysql://127.0.0.1:3306/db_master?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
          # 连接池配置(默认HikariCP)
          hikari:
            maximum-pool-size: 20
            minimum-idle: 5
        # 从库1(读,前缀slave_,用于分组负载均衡)
        slave_1:
          url: jdbc:mysql://127.0.0.1:3306/db_slave1?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 从库2(读,前缀slave_)
        slave_2:
          url: jdbc:mysql://127.0.0.1:3306/db_slave2?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 业务库(独立数据源)
        business_db:
          url: jdbc:mysql://127.0.0.1:3306/db_business?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
 
 

4. 基础使用(@DS 注解)

 
@DS 是核心注解,优先级:方法注解 > 类注解 > 默认数据源,仅在 Service 层生效(Mapper/Controller 层失效)。
 

(1)类级别注解(默认数据源)

 
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;

// 类级别:该Service所有方法默认使用business_db数据源
@Service
@DS("business_db")
public class BusinessService {
    // 无方法注解:使用类注解的business_db
    public void doBusiness() {
        // SQL操作 → 走business_db
    }
}
 
 

(2)方法级别注解(覆盖类注解)

 
java
 
运行
 
 
 
 
@Service
// 类级别默认master
// @DS("master")
public class UserService {

    // 无注解:使用默认master(写操作)
    public void addUser(User user) {
        userMapper.insert(user);
    }

    // 指定slave_1(读操作)
    @DS("slave_1")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }

    // 指定分组名slave,自动负载均衡slave_1/slave_2
    @DS("slave")
    public List<User> listAllUser() {
        return userMapper.selectList(null);
    }

    // 覆盖类注解,使用business_db
    @DS("business_db")
    public void crossDbOperation() {
        // SQL操作 → 走business_db
    }
}
 
 

(3)简化注解(读写分离)

 
@Master/@Slave@DS 的别名,简化读写分离场景:
 
java
 
运行
 
 
 
 
@Service
public class OrderService {

    // 等价于@DS("master"),强制主库写
    @Master
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }

    // 等价于@DS("slave"),从库读(负载均衡)
    @Slave
    public List<Order> listOrder(Long userId) {
        return orderMapper.selectByUserId(userId);
    }
}
 
 

三、进阶用法

 

1. API 手动切换数据源

 
适合复杂逻辑(如多租户、动态参数切换),核心类 DynamicDataSourceContextHolder
 
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.springframework.stereotype.Service;

@Service
public class TenantService {

    // 多租户按库隔离:根据租户ID切换数据源
    public void handleTenant(Long tenantId) {
        try {
            // 切换到租户专属数据源(如tenant_001)
            String dsName = "tenant_" + tenantId;
            DynamicDataSourceContextHolder.push(dsName);
            
            // 执行业务逻辑 → 走tenant_001
            queryTenantData(tenantId);
        } finally {
            // 必须出栈,恢复上下文(避免污染)
            DynamicDataSourceContextHolder.poll();
        }
    }

    // 嵌套切换数据源
    public void nestedDsOperation() {
        try {
            DynamicDataSourceContextHolder.push("slave_1");
            System.out.println("当前数据源:" + DynamicDataSourceContextHolder.peek()); // slave_1
            
            // 内层临时切换
            try {
                DynamicDataSourceContextHolder.push("slave_2");
                System.out.println("当前数据源:" + DynamicDataSourceContextHolder.peek()); // slave_2
            } finally {
                DynamicDataSourceContextHolder.poll(); // 回到slave_1
            }
        } finally {
            DynamicDataSourceContextHolder.poll(); // 回到默认master
        }
    }
}
 
 

2. 动态增删数据源(运行时配置)

 
支持项目启动后新增 / 删除数据源(多租户、动态扩容场景):
 
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;

@Component
public class DynamicDsManager {

    @Resource
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    @Resource
    private DataSourceCreator dataSourceCreator;

    // 新增数据源
    public void addDataSource(String dsName, String url, String username, String password) {
        DataSourceProperty property = new DataSourceProperty();
        property.setUrl(url);
        property.setUsername(username);
        property.setPassword(password);
        property.setDriverClassName("com.mysql.cj.jdbc.Driver");
        
        // 创建数据源并添加到管理器
        DataSource dataSource = dataSourceCreator.createDataSource(property);
        dynamicRoutingDataSource.addDataSource(dsName, dataSource);
    }

    // 删除数据源
    public void removeDataSource(String dsName) {
        dynamicRoutingDataSource.removeDataSource(dsName);
    }
}
 
 

3. 自定义负载均衡策略

 
实现 DynamicDataSourceStrategy 接口,替代内置的轮询 / 随机策略:
 
java
 
运行
 
 
 
 
import com.baomidou.dynamic.datasource.strategy.DynamicDataSourceStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;

// 自定义策略:优先选择slave_1,不可用时选slave_2
public class PriorityDsStrategy implements DynamicDataSourceStrategy {
    @Override
    public String determineDataSource(List<String> dataSources) {
        if (dataSources.contains("slave_1")) {
            return "slave_1";
        }
        return dataSources.get(0);
    }
}

// 注入自定义策略
@Configuration
public class DsConfig {
    @Bean
    public DynamicDataSourceStrategy dynamicDataSourceStrategy() {
        return new PriorityDsStrategy();
    }
}
 
 

4. 事务支持

 

(1)单数据源事务(@DSTransactional)

 
替代 Spring 原生 @Transactional,确保事务绑定当前数据源:
 
java
 
运行
 
 
 
 
@Service
public class UserService {

    // 绑定slave_1数据源的事务
    @DS("slave_1")
    @DSTransactional(rollbackFor = Exception.class)
    public void batchUpdate(List<Long> ids) {
        for (Long id : ids) {
            userMapper.updateStatus(id, 1);
            if (id == 100L) {
                throw new RuntimeException("触发回滚"); // 事务回滚
            }
        }
    }
}
 
 

(2)分布式事务(Seata + @GlobalTransactional)

 
跨多数据源事务需整合 Seata,替换 @DSTransactional@GlobalTransactional
 
java
 
运行
 
 
 
 
import io.seata.spring.annotation.GlobalTransactional;

@Service
public class OrderUserService {

    // 全局事务:跨master(订单库)和slave1(用户库)
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrderAndDeduct(Long userId, Integer amount) {
        // 切换到master创建订单
        DynamicDataSourceContextHolder.push("master");
        orderMapper.insert(new Order(userId, amount));
        
        // 切换到slave1扣减余额
        DynamicDataSourceContextHolder.push("slave1");
        userMapper.deductBalance(userId, amount);
        
        // 异常时全局回滚
        // throw new RuntimeException("模拟异常");
    }
}
 
 

四、信创适配(国产数据库)

 
只需替换驱动和连接配置,核心用法不变:
 
yaml
 
 
spring:
  datasource:
    dynamic:
      datasource:
        # 达梦数据库
        dm_db:
          url: jdbc:dm://192.168.1.100:5236/DB_NAME?SYSDBA=1
          username: SYSDBA
          password: DAMENG123
          driver-class-name: dm.jdbc.driver.DmDriver
        # 人大金仓
        kingbase_db:
          url: jdbc:kingbase8://192.168.1.101:54321/DB_NAME
          username: SYSTEM
          password: 123456
          driver-class-name: com.kingbase8.Driver
 
 

五、避坑指南(高频问题)

 

1. @DS 注解不生效?

 
  • ❌ 注解加在 Mapper/Controller 层 → ✅ 仅加在 Service 层;
  • ❌ 同类内部调用加注解的方法 → ✅ 通过 Bean 调用(AOP 生效);
  • ❌ 数据源名称拼写错误 → ✅ 检查配置文件的数据源 Key;
  • ❌ 事务内切换数据源 → ✅ 事务外切换(事务绑定初始数据源)。
 

2. 多线程数据源串库?

 
  • 原因:ThreadLocal 线程私有,线程池复用导致上下文残留;
  • 解决:线程任务结束前调用 DynamicDataSourceContextHolder.clear()
 

3. 负载均衡策略不生效?

 
  • ❌ 数据源名称无下划线前缀(如 slave1)→ ✅ 配置为 slave_1
  • ❌ 直接指定具体数据源(@DS("slave_1"))→ ✅ 指定分组名(@DS("slave"))。
 

4. 分布式事务不生效?

 
  • ❌ 未代理动态数据源 → ✅ 用 DataSourceProxy 包裹 DynamicRoutingDataSource
  • ❌ XID 未透传 → ✅ 线程池任务中手动绑定 XID。
 

六、核心配置项速查

 
表格
 
配置项默认值说明
spring.datasource.dynamic.primary master 默认数据源名称
spring.datasource.dynamic.strict false 找不到数据源时是否抛异常
spring.datasource.dynamic.strategy LoadBalanceDynamicDataSourceStrategy 全局负载均衡策略
spring.datasource.dynamic.datasource - 多数据源配置列表
spring.datasource.dynamic.type HikariCP 数据源连接池类型
 

总结

 
  1. 核心定位:dynamic-datasource-spring-boot-starter 是 Spring Boot 多数据源的首选组件,无侵入、配置简单;
  2. 核心用法:@DS 注解(Service 层)实现静态切换,DynamicDataSourceContextHolder API 实现动态切换;
  3. 关键原则:@DS 仅在 Service 层生效,事务需用 @DSTransactional/@GlobalTransactional
  4. 进阶能力:支持动态增删数据源、自定义负载均衡、国产数据库适配、分布式事务整合。

-------------------------------------------------------------------------

-------------------------------------------------------------------------

posted @ 2026-03-11 09:15  hanease  阅读(724)  评论(0)    收藏  举报