SpringBoot+MybatisPlus动态数据源

dynamic-datasource 使用文档

本文档基于本项目(Spring Boot 4.1.1 + dynamic-datasource 4.5.0 + MyBatis-Plus 3.5.17)整理,涵盖依赖导入、配置、DataSourceManager 工具类、@DS 注解与编程式两种切换方式,以及其他常用扩展用法。

目录

  • [1. 简介](#1. 简介)
  • [2. Maven 依赖](#2. Maven 依赖)
  • [3. properties 配置](#3. properties 配置)
  • [4. DataSourceManager 工具类](#4. DataSourceManager 工具类)
  • [5. 使用教程一:@DS 注解切换](#5. 使用教程一:@DS 注解切换)
  • [6. 使用教程二:DataSourceManager 编程式切换](#6. 使用教程二:DataSourceManager 编程式切换)
  • [7. 其他用法](#7. 其他用法)
  • [8. 常见坑](#8. 常见坑)

1. 简介

dynamic-datasource 是苞米豆(baomidou)开源的多数据源解决方案,核心思路是:

  • 容器中只存在一个 DynamicRoutingDataSource(路由数据源),内部持有一个 Map<String, DataSource>
  • 每次获取连接时,根据 ThreadLocal 上下文DynamicDataSourceContextHolder)中保存的数据源名称,从 Map 中取出真实的 DataSource 返回;
  • 上下文是一个 栈结构(push / poll),天然支持嵌套切换和自动恢复。

因此它可以在运行时:

  1. 动态注册/移除数据源(往 Map 里 put / remove);
  2. 动态切换数据源(往 ThreadLocal 里 push 数据源名)。

2. Maven 依赖

Spring Boot 4.x 必须使用 dynamic-datasource-spring-boot4-starter(对应 Spring Boot 2/3 分别是 spring-boot-starter / spring-boot3-starter):

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

<!-- 可选:MyBatis-Plus,本文档示例基于其 BaseMapper 查询 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
    <version>3.5.17</version>
</dependency>

<!-- 可选:MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

引入 starter 后自动配置即生效,无需任何注解或 @Configuration

3. properties 配置

引入 starter 后,原生的 spring.datasource.* 配置会被接管,必须迁移到 spring.datasource.dynamic.* 前缀下,并且必须至少配置一个名为 master 的主数据源(primary 指向它):

properties 复制代码
spring.application.name=demo

# ---------- dynamic-datasource 核心配置 ----------
# 未指定数据源时使用的默认数据源名
spring.datasource.dynamic.primary=master
# 严格匹配模式:设为 true 时,切换到不存在的数据源会抛异常(false 时回退到 primary)
spring.datasource.dynamic.strict=false

# 主数据源 master(名字可自定义,但必须与 primary 一致)
spring.datasource.dynamic.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.master.url=jdbc:mysql://localhost:3306/demo?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
spring.datasource.dynamic.datasource.master.username=root
spring.datasource.dynamic.datasource.master.password=root

# ---------- MyBatis-Plus ----------
mybatis-plus.mapper-locations[0]=classpath*:/mapper/**/*.xml
mybatis-plus.type-aliases-package=org.gudian.user
mybatis-plus.configuration.map-underscore-to-camel-case=true
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

配置多个静态数据源(扩展)

如果数据源在编码期就已知,可直接在 properties 中并列配置多个(本项目是运行时动态注册,故只配 master):

properties 复制代码
spring.datasource.dynamic.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.slave.url=jdbc:mysql://localhost:3306/demo2?characterEncoding=utf8&useSSL=false&serverTimezone=UTC
spring.datasource.dynamic.datasource.slave.username=root
spring.datasource.dynamic.datasource.slave.password=root

常用可选配置

配置项 说明
spring.datasource.dynamic.strict 严格模式。true 时查找不到数据源抛异常,false 时回退 primary
spring.datasource.dynamic.datasource.<name>.lazy 该数据源是否懒加载(默认启动时立即初始化连接池)
spring.datasource.dynamic.datasource.<name>.pool-type 连接池类型,默认 HikariCP(也支持 Druid、Dbcp2 等,需另引依赖)
spring.datasource.dynamic.seata 是否开启 Seata 分布式事务支持

4. DataSourceManager 工具类

DataSourceManager 是本项目对 dynamic-datasource 底层 API 的封装,提供了线程安全的编程式切换 (自动切换、自动恢复)和运行时动态注册/移除数据源能力。

完整代码见 DataSourceManager.java

java 复制代码
package org.gudian.datasource;

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.gudian.datasource.exception.DataSourceAlreadyExistsException;
import org.gudian.datasource.exception.DataSourceNotFoundException;
import org.gudian.datasource.exception.DataSourceRegisterException;
import org.gudian.datasource.exception.IllegalDataSourceOperationException;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Set;
import java.util.function.Supplier;

@Component
public class DataSourceManager {

    /** 主数据源名,与 spring.datasource.dynamic.primary 保持一致 */
    public static final String PRIMARY_DATA_SOURCE = "master";

    private final DynamicRoutingDataSource routingDataSource;
    private final DefaultDataSourceCreator dataSourceCreator;

    public DataSourceManager(DataSource dataSource, DefaultDataSourceCreator dataSourceCreator) {
        // 自动配置中 dataSource 的声明类型为 javax.sql.DataSource,实际为 DynamicRoutingDataSource,
        // 直接按具体类型注入会因声明类型不匹配而找不到 bean,因此注入后强转
        if (!(dataSource instanceof DynamicRoutingDataSource routing)) {
            throw new IllegalStateException("当前 DataSource 不是 DynamicRoutingDataSource,无法进行动态数据源管理");
        }
        this.routingDataSource = routing;
        this.dataSourceCreator = dataSourceCreator;
    }

    /**
     * 编程式切换:在指定数据源上执行 Runnable,执行完自动恢复原数据源
     */
    public void executeInDs(String dsName, Runnable action) {
        DynamicDataSourceContextHolder.push(dsName);
        try {
            action.run();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    /**
     * 编程式切换:在指定数据源上执行 Supplier 并返回结果,执行完自动恢复原数据源
     */
    public <T> T executeInDs(String dsName, Supplier<T> action) {
        DynamicDataSourceContextHolder.push(dsName);
        try {
            return action.get();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }

    /**
     * 运行时注册数据源(不持久化,重启后失效)
     */
    public void register(String name, String url, String username, String password) {
        if (isBlank(name) || isBlank(url) || isBlank(username) || isBlank(password)) {
            throw new IllegalDataSourceOperationException("name、url、username、password 均不能为空");
        }
        if (contains(name)) {
            throw new DataSourceAlreadyExistsException(name);
        }
        DataSourceProperty property = new DataSourceProperty();
        property.setPoolName(name);
        property.setUrl(url);
        property.setUsername(username);
        property.setPassword(password);
        property.setDriverClassName("com.mysql.cj.jdbc.Driver");
        DataSource created = null;
        try {
            created = dataSourceCreator.createDataSource(property);
            try (Connection connection = created.getConnection()) {
                // 能成功获取连接即视为数据源可用(连接验证,连不上会抛异常)
            }
        } catch (Exception e) {
            closeQuietly(created);
            throw new DataSourceRegisterException(name, e);
        }
        routingDataSource.addDataSource(name, created);
    }

    /**
     * 移除数据源(主数据源 master 不允许移除)
     */
    public void remove(String name) {
        if (PRIMARY_DATA_SOURCE.equals(name)) {
            throw new IllegalDataSourceOperationException("主数据源不允许移除: " + name);
        }
        if (!contains(name)) {
            throw new DataSourceNotFoundException(name);
        }
        routingDataSource.removeDataSource(name);
    }

    /** 数据源是否已存在 */
    public boolean contains(String name) {
        return routingDataSource.getDataSources().containsKey(name);
    }

    /** 列出所有已注册的数据源名 */
    public Set<String> list() {
        return Set.copyOf(routingDataSource.getDataSources().keySet());
    }

    private void closeQuietly(DataSource dataSource) {
        if (dataSource instanceof AutoCloseable closeable) {
            try {
                closeable.close();
            } catch (Exception ignored) {
                // 注册失败后的资源清理失败可忽略
            }
        }
    }

    private static boolean isBlank(String value) {
        return value == null || value.isBlank();
    }
}

设计要点说明

要点 说明
构造器注入 DataSource 后强转 自动配置注册的 dataSource bean 声明类型是 javax.sql.DataSource 父类型,按 DynamicRoutingDataSource 具体类型注入会报找不到 bean,必须注入父类型后 instanceof 强转
push / poll 配对 push 压栈 → 执行 → finally 中 poll 出栈。即使业务代码抛异常,finally 也保证恢复,不会污染线程(Tomcat 线程池复用线程)
嵌套调用安全 栈结构支持嵌套:executeInDs("a", () -> executeInDs("b", ...)),内层执行完回到 a,外层执行完回到最初
注册时验证连接 createDataSource 后先 getConnection() 试连,连不上则关闭连接池并抛 DataSourceRegisterException,避免注册进无效数据源
注册不持久化 数据源只存于内存 Map,应用重启后动态注册的数据源消失(本项目的需求即如此)

5. 使用教程一:@DS 注解切换

@DS("数据源名") 是最常用的声明式切换方式,基于 AOP 实现,可标注在方法上(方法优先于类)。

5.1 基本用法

java 复制代码
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderMapper orderMapper;

    // 方法级注解:本方法走 slave 数据源
    @DS("slave")
    public List<Order> queryFromSlave() {
        return orderMapper.selectList(null);
    }

    // 未标注:走 primary(master)数据源
    public List<Order> queryFromMaster() {
        return orderMapper.selectList(null);
    }
}

5.2 类级注解(读写分离典型用法)

java 复制代码
// 整个 Service 默认走 slave,个别方法可再覆盖
@DS("slave")
@Service
public class ReportService {

    public void generate() { /* 走 slave */ }

    @DS("master")   // 方法注解覆盖类注解
    public void save() { /* 走 master */ }
}

5.3 @DS("master") 与 @Master

切到其他数据源后想明确回到主库,除 @DS("master") 外还可以用框架提供的 @Master 注解,语义更清晰:

java 复制代码
@Master
public void writeOnMaster() { /* 走 primary 数据源 */ }

5.4 注意事项

  • 同类内部自调用失效@DS 是 AOP 代理实现的,this.methodB() 不会走代理,注解不生效。解决方式:注入自身代理、拆到另一个 Bean、或改用 DataSourceManager.executeInDs
  • 不要与编程式切换混用在同一方法executeInDs 的 push 压在 @DS 之上时,以 executeInDs 的为准(内层覆盖外层),容易造成误解;
  • 注解值必须是已存在 的数据源名(静态配置的或已动态注册的),不存在时依 strict 配置决定抛异常还是回退 primary。

6. 使用教程二:DataSourceManager 编程式切换

适用于运行时才能确定数据源的场景(如按租户路由、用户传入数据源名等)。本项目即采用此方案。

6.1 注入并使用

java 复制代码
@Service
@RequiredArgsConstructor
public class UserQueryService {

    private final UserMapper userMapper;
    private final DataSourceManager dataSourceManager;

    public List<User> queryUsers(String dsName) {
        // 1. 先校验数据源存在
        if (!dataSourceManager.contains(dsName)) {
            throw new DataSourceNotFoundException(dsName);
        }
        // 2. 在指定数据源上执行查询,执行完自动恢复
        return dataSourceManager.executeInDs(dsName, () -> userMapper.selectList(null));
    }
}

无返回值的操作用 Runnable 重载:

java 复制代码
dataSourceManager.executeInDs("db2", () -> userMapper.insert(new User()));

6.2 动态注册/移除数据源(REST API)

本项目通过 DataSourceController.java 暴露了管理接口:

接口 方法 说明
/api/datasource POST 注册数据源,body:{"name":"db2","url":"jdbc:mysql://...","username":"root","password":"root"}
/api/datasource GET 列出所有数据源名
/api/datasource/{name} DELETE 移除数据源(master 受保护)

注册后会立即试连验证,连不上返回 400。

6.3 完整验证流程(curl)

bash 复制代码
# 1. 查询主库
curl "http://localhost:8080/api/users?ds=master"
# [{"age":20,"id":1,"name":"alice"}]

# 2. 动态注册指向 demo2 库的数据源 db2
curl -X POST "http://localhost:8080/api/datasource" \
  -H "Content-Type: application/json" \
  -d '{"name":"db2","url":"jdbc:mysql://localhost:3306/demo2?characterEncoding=utf8&useSSL=false&serverTimezone=UTC","username":"root","password":"root"}'
# {"message":"数据源注册成功: db2"}

# 3. 切换到 db2 查询
curl "http://localhost:8080/api/users?ds=db2"
# [{"age":30,"id":1,"name":"bob"}]

# 4. 查看数据源列表
curl "http://localhost:8080/api/datasource"
# ["db2","master"]

# 5. 移除 db2
curl -X DELETE "http://localhost:8080/api/datasource/db2"

7. 其他用法

7.1 直接使用底层 API(不经过 DataSourceManager)

如果不想封装,也可以直接操作 DynamicDataSourceContextHolder,但必须自己保证 push/poll 配对(推荐 try-finally):

java 复制代码
DynamicDataSourceContextHolder.push("db2");
try {
    // 此区间内的所有 MyBatis/事务操作都走 db2
    List<User> users = userMapper.selectList(null);
} finally {
    DynamicDataSourceContextHolder.poll();  // 出栈恢复,切勿用 clear()
}

clear() 会清空整个栈,在嵌套场景会破坏外层的数据源状态,一般只在请求拦截器收尾兜底时使用。

7.2 跨数据源事务:@DSTransactional

Spring 原生 @Transactional 开启后会绑定单个连接,事务内切换数据源不再生效 。若需要跨库操作且各自独立提交/回滚,使用框架提供的 @DSTransactional

java 复制代码
@DSTransactional
public void crossDbWrite() {
    userMapper.insert(userA);          // 走 master
    slaveMapper.insert(userB);        // 走 slave(需 @DS("slave") 或编程式切换)
    // 任意一步抛异常,已执行的操作全部回滚
}

注意 @DSTransactional 是"多数据源各自事务 + 最终一致回滚",不是真正的分布式强一致事务;后者需引入 Seata。

7.3 嵌套切换(栈机制示例)

java 复制代码
dataSourceManager.executeInDs("db1", () -> {
    // 此处走 db1
    dataSourceManager.executeInDs("db2", () -> {
        // 此处走 db2(内层覆盖外层)
    });
    // 此处恢复走 db1
});
// 此处恢复走 primary

7.4 请求级拦截器切换(扩展思路)

可以自定义拦截器从请求头解析租户/数据源名,请求开始时 push、结束时 poll,实现业务代码零感知的切换:

java 复制代码
@Component
public class DsInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String ds = request.getHeader("X-DataSource");
        if (ds != null) {
            DynamicDataSourceContextHolder.push(ds);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) {
        DynamicDataSourceContextHolder.poll();
    }
}

7.5 编程式添加静态配置数据源的等价操作

register() 内部使用的 DefaultDataSourceCreator.createDataSource(DataSourceProperty) 也可以用于读取自定义配置(如数据库里的租户连接信息表)批量初始化数据源,DataSourceProperty 常用属性:

属性 说明
poolName 连接池/数据源名
url / username / password 连接信息
driverClassName 驱动类(可省略,框架会从 url 推断)
lazy 是否懒加载
initMethod / fault 初始化方法 / 是否为兜底数据源

7.6 动态数据源监控

DynamicRoutingDataSource.getDataSources() 返回 Map<String, DataSource>,可用于自检、健康检查或对接 Spring Boot Actuator(引入 actuator 后 dynamic-datasource 自动注册连接池指标)。

8. 常见坑

现象 解决
DynamicRoutingDataSource 具体类型注入失败 启动报 No qualifying bean of type 'DynamicRoutingDataSource' 自动配置 bean 声明类型是 javax.sql.DataSource,注入父类型后强转(见 DataSourceManager 构造器)
@Transactional 内切换数据源无效 事务内查询始终走事务开启时绑定的库 事务开启前切换,或改用 @DSTransactional
同类自调用 @DS 失效 AOP 代理不拦截 this.xxx() 拆 Bean、注入自身代理、或用 executeInDs
动态注册的数据源重启后消失 数据源只存于内存 如需持久化,自行保存连接信息并在启动时(如 ApplicationRunner)重新 register
切换到不存在的数据源 strict=false 静默回退 primary,可能导致查错库 生产建议 spring.datasource.dynamic.strict=true
push 后忘记 poll 线程池复用线程,后续请求拿到脏数据源 始终用 try-finally 或直接使用 DataSourceManager.executeInDs
相关推荐
小义_16 分钟前
JDK 深度解析
java·linux·开发语言·python·面试
奈斯先生Vector36 分钟前
从“能调用”到“可运营”:AI Agent 进入多模型时代后的架构升级
java·javascript·数据库·人工智能·算法·架构·aigc
Json____39 分钟前
宿舍安全卫生检查系统:Node 全栈开发实战
java·前端·数据库·毕业设计·课程设计·毕设·wwwoop.com
pnoker42 分钟前
36 个驱动模块:应对协议碎片化
java·物联网·modbus·工业互联网·opc ua
码上有光43 分钟前
Linux:进程控制和进程替换
java·linux·服务器·进程控制·进程替换
默辨1 小时前
我用Java后端的视角读了一遍JoyAgent-JDGenie
java·开发语言
SQL-First布道者1 小时前
⚡ Spring JDBC 完整体系 · 第 5 讲 · Spring Boot Starter JDBC
java·spring boot·spring·mybatis·spring jdbc
宋哥转AI1 小时前
深入理解 AI Agent · 多 Agent 编排 #03:Supervisor 与 Orchestrator——Dream-SaaS 双层编排架构拆解
java·人工智能·ai
木井巳1 小时前
【JavaEE】Spring Web MVC 入门
java·spring boot·spring·servlet·java-ee