
简介面向需要在Spring Boot项目中集成多数据源并实现手动切换的开发者这份示例代码围绕dynamic-datasource框架演示MySQL与SQL Server之间的动态数据源配置与切换方法也涵盖动态添加、删除数据源的实现思路适合有Spring Boot基础、正在处理多数据库读写场景的读者参考。压缩包共23个文件以17个Java源码文件为主配合XML映射文件、YML配置文件以及MD说明文档结构清晰便于直接查看核心切换逻辑与工程配置。资源包整体仅20KB轻量易用下载后即可快速导入工程进行测试。目前已有552人学习该资源。代码中提供了完整的多数据源手动切换示例包括数据源注册、切换注解使用、配置项组织等关键内容同时附带项目说明文档可帮助读者理解从配置到实现的完整链路减少在真实业务中整合多数据源的踩坑成本。1. 为什么需要手动切换多数据源从dynamic-datasource说起开发中经常遇到一个服务要同时访问两个不同的数据库典型场景是业务系统的主库用 mysql而生鲜报表或历史数据落在 sqlserver 上。SpringBoot 默认的单数据源配置没法支撑这种双库读写最容易想到的方案是把两个数据源都配置好然后用一个路由类在代码里切换。dynamic-datasource 正是为此设计的轻量级框架它在 Spring 的 AbstractRoutingDataSource 之上做了封装让数据源路由变成线程上下文里的一个 key。但它最常被提到的能力是 DS 注解这个注解适合方法或类级别固定选择的场景如果要按请求参数、线程变量或外部配置在运行过程中决定走哪个库DS 就僵住了只能靠手动切换。下面的内容会把动态数据源的配置、手动切换的 API 边界、事务与线程池中的失效问题以及 mysql 和 sqlserver 双数据源的完整示例代码讲清楚按步骤复现就能拿到一个可运行的多数据源 Demo。2. 集成 dynamic-datasource依赖、yaml 配置与自动路由原理先把 dynamic-datasource 在 SpringBoot 里的工作方式说清楚。它引入一个 starter 依赖后启动时会扫描 spring.datasource.dynamic 下的所有配置为每个 key 创建独立的连接池并注册一个 DynamicRoutingDataSource 作为主数据源替代 SpringBoot 自动配置的默认数据源。业务代码里通过一个 key 来决定走哪个连接池这个 key 可以从 ThreadLocal 里读取也可以由 DS 注解在前置拦截时塞进去。理解了这套机制后面手动切换就水到渠成。2.1 添加依赖与 SpringBoot 自动配置在 pom.xml 中引入 dynamic-datasource 的 starterSpringBoot 2.x 和 3.x 都能用starter 会根据当前 Spring 版本选择适配路径。对已经使用 MyBatis-Plus 的项目来说这个依赖不会冲突因为它不强制介入 ORM 层JdbcTemplate、MyBatis 可以照常用。dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version${dynamic-datasource.version}/version /dependency版本号建议去 Maven 仓库查最新与当前 SpringBoot 版本兼容的版本。引入依赖后SpringBoot 的 DataSourceAutoConfiguration 会被 DynamicRoutingDataSource 接管你如果在 application.yml 里继续配置了 spring.datasource.url 这类旧属性它们会被忽略。这是 dynamic-datasource 官方设计的行为目的就是让所有连接都经过路由层。2.2 多数据源 yaml 配置mysql 作为默认库sqlserver 作为目标库在 application.yml 里配置两个数据源这是整个 SpringBoot 配置里最核心的部分。默认数据源设为 mysql另一个 key 命名为 sqlserver所有查询如果不做手动切换都会落到 mysql。spring: datasource: dynamic: primary: mysql strict: true datasource: mysql: url: jdbc:mysql://localhost:3306/business_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver sqlserver: url: jdbc:sqlserver://localhost:1433;databaseNamereport_db;encrypttrue;trustServerCertificatetrue username: sa password: 123456 driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver hikari: max-pool-size: 10 min-idle: 5 connection-timeout: 30000配置说明primary 指定默认 keystrict 为 true 时如果代码里 push 了一个不存在的 key会立刻抛出 DataSourceNotFoundException这个特性在手动切换时非常有用能快速暴露写错的 key 而不是悄悄落到默认库。datasource 下的每个 key 对应一个独立连接池key 名称完全自定义但建议和数据库类型或业务模块保持一致。sqlserver 的 url 在高版本数据库上必须带 encrypttrue否则连接会被拒绝trustServerCertificatetrue 用于跳过自签名证书校验本地联调时省去配置证书的麻烦。Hikari 连接池的参数是全局生效的针对某个数据源单独调优时可以把 hikari 节点移到对应的 datasource.xxx 内部。提示如果同时保留 spring.datasource.url 配置dynamic-datasource 会自动忽略旧配置避免两者互相干扰。最佳实践是删除单数据源配置只保留 dynamic 节点。2.3 路由原理从 AbstractRoutingDataSource 到 ThreadLocal 的 keydynamic-datasource 的底层核心是动态路由数据源它继承 Spring 提供的 AbstractRoutingDataSource。这个抽象类在每次获取连接时都会调用 determineCurrentLookupKey() 方法返回一个用于定位目标数据源的 key。dynamic-datasource 对这个方法做了重写返回的是当前线程上下文栈顶的元素。手动切换的本质就是操作这个线程上下文栈。DynamicDataSourceContextHolder 维护了一个 ThreadLocal 栈提供 push、poll、peek 三个操作。DS 注解其实也是通过 AOP 切面在方法执行前 push方法执行后 poll和手动调用 API 是同一套机制。区别只在于触发时机注解是编译期写死的手动切换可以放在运行时的分支逻辑里。步骤动作说明1调用 DynamicDataSourceContextHolder.push(key)把目标数据源 key 压入当前线程栈顶2执行数据库操作获取连接时 determineCurrentLookupKey() 返回栈顶 key3从目标数据源 Map 中找到对应连接池key 不存在且 stricttrue 时抛异常4调用 DynamicDataSourceContextHolder.poll()弹出栈顶 key恢复切换前状态手动切换的代码量和成本都集中在 push 和 poll 的配对管理上这也是下面章节要重点展开的部分。3. 手动切换的核心 APIDynamicDataSourceContextHolder 的 push、poll 与坑dynamic-datasource 提供的手动切换 API 非常简单常用到的只有三个静态方法。但简单背后藏着几个容易踩雷的边界条件不搞明白这些边界切换时好时坏的情况就会出现。下面逐个拆解并给出一段可以直接运行的安全写法。3.1 push、poll、peek 的职责与标准写法三个方法的职责分别是在当前线程上下文中压入 key、弹出 key、查看栈顶 key。由于底层是栈结构push 可以嵌套调用。比如先 push mysql再 push sqlserver此时 peek 返回 sqlserverpoll 返回 sqlserver 的同时栈顶回到 mysql。import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; import org.springframework.jdbc.core.JdbcTemplate; public class OrderService { private final JdbcTemplate jdbcTemplate; public OrderService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } public ListMapString, Object loadReportData(String orderId) { // 将当前线程的数据源切换到 sqlserver DynamicDataSourceContextHolder.push(sqlserver); try { return jdbcTemplate.queryForList( SELECT * FROM dbo.order_report WHERE order_id ?, orderId); } finally { // 确保任何情况下都弹出 key防止线程污染 DynamicDataSourceContextHolder.poll(); } } }这段代码的逻辑很直白先 push sqlserver再通过 JdbcTemplate 执行查询。由于 JdbcTemplate 内部获取连接时使用的是 DynamicRoutingDataSource此时会拿到 sqlserver 连接。finally 块保证即使 sqlquery 抛出异常poll 也会执行当前线程的数据源状态不会残留。参数说明queryForList 的第二个参数是查询条件的绑定变量避免手工拼接字符串引入 SQL 注入风险。如果业务中还使用了 MyBatis-Plus那只需要在 Mapper 接口上不加 DS 注解在 Service 里手动 push/poll效果完全一样因为 MyBatis 的 SqlSession 获取连接同样会走路由层。3.2 事务会把手动切换变成无效操作很常见的场景是Service 方法上加了 Transactional方法内部手动切换 key 并查询结果查出来还是默认库的数据。原因在于 Spring 事务管理器在事务开启时就会从数据源获取一个连接并把这个连接绑定到当前线程的 ResourceHolder 中。事务提交或回滚前所有数据库操作都复用这一条物理连接后续再怎么切换 key也不会改变已绑定连接的归属。场景切换时机实际效果无事务方法push 后查询finally poll切换成功事务方法外 push先手动切换再进入事务方法事务连接按 push 的 key 获取切换成功事务方法内 push事务已开启再执行 push 查询切换无效连接已被事务持有嵌套事务内外方法不同数据源需要自定义事务管理器按 key 路由否则回滚错乱要避开这个坑最直接的办法是让手动切换代码所在的方法不要加 Transactional。如果确实需要事务保护就把数据源切换动作放在事务传播边界的上层由切面或调用方统一 push/poll事务方法内部不做二次切换。另一种方案是针对不同数据源分别定义 PlatformTransactionManager然后用 Transactional(transactionManager sqlserverTxManager) 指定事务管理器这种方式在跨数据源一致性要求不高时足够用但两个库之间的事务回滚无法保证原子性。3.3 线程池复用会让 ThreadLocal 里的 key 凭空消失线程池中的子线程不会继承父线程的 ThreadLocal 值这是 Java 层早就约定好的规则。dynamic-datasource 的上下文存在 ThreadLocal 里所以如果你在当前线程里 push 了 key再提交一个任务到线程池子线程执行时 peek 的结果是 null默认会落到 primary 库。ExecutorService executor Executors.newFixedThreadPool(4); DynamicDataSourceContextHolder.push(sqlserver); try { String ds DynamicDataSourceContextHolder.peek(); executor.submit(() - { // 子线程中不能直接读到父线程 push 的 key需要手动传值 DynamicDataSourceContextHolder.push(ds); try { jdbcTemplate.queryForList(SELECT * FROM dbo.orders); } finally { DynamicDataSourceContextHolder.poll(); } }); } finally { DynamicDataSourceContextHolder.poll(); }上面这段代码的处理方式是把栈顶 key 用局部变量 ds 捕获传入子线程后再 push 一次。这种方式侵入性较强但逻辑最清晰。如果项目里大量使用线程池可以考虑用 TransmittableThreadLocal 包装 dynamic-datasource 的上下文但那样需要替换内部组件维护成本偏高一般不建议为了一个 key 引入额外依赖。更务实的做法是显式传递数据源标识让线程任务自己决定用哪个数据源。4. 完整示例mysql 与 sqlserver 手动切换的落地代码这一章把前面的原理和 API 拼成一个能启动的 Demo。我们会从项目结构开始提供数据库脚本、Service 层代码、Controller 接口和验证方法方便你直接照搬调试。4.1 项目结构与数据库准备项目使用 SpringBoot 外加 spring-boot-starter-jdbc不需要集成 ORM 也能验证切换效果。主要文件结构如下src/main/java/com/example/demo ├── DemoApplication.java ├── controller/ │ └── DataSourceController.java └── service/ └── QueryService.java在 mysql 的 business_db 库中建一张 user 表在 sqlserver 的 report_db 库中也建一张同名表两张表的数据故意不一样结果便于区分。-- mysql 执行 CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8; INSERT INTO user(name) VALUES (mysql-user-1); -- sqlserver 执行 CREATE TABLE [dbo].[user] ( [id] bigint IDENTITY(1,1) NOT NULL, [name] nvarchar(50) NULL, CONSTRAINT [PK_user] PRIMARY KEY ([id]) ); INSERT INTO [dbo].[user] ([name]) VALUES (sqlserver-user-1);数据库脚本说明两张表字段名和类型尽量保持一致但 SQL Server 的 nvarchar 与 mysql 的 varchar 有细微差异这是两个数据库的固有特性不影响查询结果。实际业务中如果存在跨库联查需要考虑字符集和排序规则的一致性这不在本次示例范围内。4.2 Service 层封装手动切换逻辑QueryService 提供两个方法一个查询默认库一个手动切到 sqlserver。这里使用 JdbcTemplate 演示换成 MyBatis 的 Mapper 也是同样的效果。import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; Service public class QueryService { private final JdbcTemplate jdbcTemplate; public QueryService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } public ListMapString, Object queryFromMysql() { // 不主动切换默认数据源就是 mysql return jdbcTemplate.queryForList(SELECT id, name FROM user); } public ListMapString, Object queryFromSqlserver() { DynamicDataSourceContextHolder.push(sqlserver); try { return jdbcTemplate.queryForList(SELECT id, name FROM [dbo].[user]); } finally { DynamicDataSourceContextHolder.poll(); } } }这段代码里有两个细节。第一mysql 的表名 user 是数据库关键字所以用反引号括起来sqlserver 用中括号括起 schema 和表名避免语法冲突。第二queryFromSqlserver 方法内部的 push 和 poll 严格配对即使查询抛异常poll 也会执行这样线程池复用该线程时不会残留 sqlserver 状态。4.3 Controller 层提供验证接口写一个简单的 REST 接口通过请求参数 ds 决定调用哪个 Service 方法这样用 curl 就能验证切换是否生效。import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; RestController RequestMapping(/demo) public class DataSourceController { private final QueryService queryService; public DataSourceController(QueryService queryService) { this.queryService queryService; } GetMapping(/query) public ListMapString, Object query(RequestParam(defaultValue mysql) String ds) { if (sqlserver.equals(ds)) { return queryService.queryFromSqlserver(); } return queryService.queryFromMysql(); } }启动应用后分别执行命令curl http://localhost:8080/demo/query?dsmysql返回结果是 mysql 表中的记录。再执行curl http://localhost:8080/demo/query?dssqlserver返回结果是 sqlserver 表中的记录。如果两个结果数据不同说明手动切换成功。这里 Controller 的逻辑简单只是为了验证。在实际项目中ds 参数往往来自请求头、租户 ID 或登录用户信息可以封装到一个 Filter 中统一处理下一章会给出一个通用的实现方式。4.4 常见失败原因与排查方向现象可能原因排查方向抛出 DataSourceNotFoundExceptionpush 的 key 与 yaml 中配置的 datasource 名称不一致打印 DynamicDataSourceContextHolder.peek() 检查实际 key查询结果还是默认库方法上存在 Transactional连接被事务提前绑定去掉事务注解或将切换放在事务方法外层偶发串库线程池复用某次 push 后 poll 未被调用检查所有 try-catch 分支是否都有 finally pollsqlserver 连接失败驱动未引入或 url 缺少 encrypt 参数确认 mssql-jdbc 依赖以及 trustServerCertificatetruemysql 和 sqlserver 都失败连接池参数超出数据库最大连接数查看 hikari 配置和数据库活动会话数排查多数据源问题时最直观的工具就是在切换方法里打印当前 peek 值排查效率会明显提升。后续进阶内容里会演示如何通过一个 Filter 统一完成这个动作避免每个方法都去写调试代码。5. 进阶技巧用 Filter 统一管理手动切换并验证切换是否生效前四章的手动切换写法在小型项目里够用但每个方法都写 try-finally 显得重复也容易出现漏 poll。这一章给出一种更优雅的工程化做法用 Filter 拦截请求从请求头中读取数据源标识自动完成 push 和 poll业务代码里不需要任何切换逻辑。同时提供一个切换验证技巧保证每次请求都能在日志中看到实际使用的数据源。5.1 用 Filter 实现请求级自动切换先写一个 Filter 类在请求进入 Controller 之前读取 X-DS 请求头如果值不为空就 push 到数据源上下文请求处理完成后在 finally 中 poll。import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; public class DataSourceSwitchFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; String ds req.getHeader(X-DS); if (ds ! null !ds.isEmpty()) { DynamicDataSourceContextHolder.push(ds); } try { chain.doFilter(request, response); } finally { if (ds ! null !ds.isEmpty()) { DynamicDataSourceContextHolder.poll(); } } } }这个 Filter 的逻辑本身不复杂但它的注册顺序很重要。如果 Spring 的事务过滤器先执行并开启了事务后续 Filter 里 push 就来不及了。因此建议用 FilterRegistrationBean 注册并将 order 设置成最小整数保证它在 OpenEntityManagerInViewFilter 或事务代理之前执行。Bean public FilterRegistrationBeanDataSourceSwitchFilter dsFilter() { FilterRegistrationBeanDataSourceSwitchFilter registration new FilterRegistrationBean(); registration.setFilter(new DataSourceSwitchFilter()); registration.addUrlPatterns(/demo/*); registration.setOrder(Integer.MIN_VALUE); return registration; }参数说明addUrlPatterns 里的路径可以根据实际业务范围收窄order 越小越先执行。这里只拦截 /demo 路径避免影响所有请求。Filter 内 push 的 key 会从请求头一直传递到 Controller 和 Service业务方法无需感知数据源切换。5.2 验证切换是否生效在日志中输出当前数据源 key自动切换的缺点是出现问题时不容易看出请求走了哪个数据源。可以在 Filter 的 finally 之前记录一行日志也可以利用 dynamic-datasource 自带的日志拦截器。更简单的方式是在业务代码入口处打印 peek 值。Object currentDs DynamicDataSourceContextHolder.peek(); log.info(当前数据源 key: {}, currentDs);这条日志在调接口时能直观看到数据源选择结果。如果发现 key 为 null说明请求没有触发 push默认会走 primary 配置。结合请求头传入的 key就能判断 Filter 是否生效。生产环境建议用 debug 级别避免日志刷屏。5.3 连接池健康检查与切换兜底多数据源环境下sqlserver 连接池可能因为网络抖动或服务重启导致连接不可用。手动切换遇到这种场景时程序会抛出连接超时异常而不是自动回退到默认库。针对这种情况可以在 Filter 里捕获数据源切换时的异常并设置一个 fallback 数据源 key例如请求 sqlserver 失败时尝试走 mysql 的只读副本。这个方案需要配合 health-check SQL 使用对连接池执行 SELECT 1 探活失败时修改 key再重新 push。健康检查可以定时执行也可以利用 Hikari 的 connection-test-query 机制。通常做法是连接池访问前检查或每隔一定时间遍历 DynamicRoutingDataSource 中的数据源 Map。动态路由数据源提供了 getDataSources 方法可以拿到所有的 key 和连接池结合 Spring 的 ScheduledExecutor 做定时探测。落地时注意这种操作会持有连接探活频率不要超过每 30 秒一次。手动切换多数据源的核心在于“记住切及时还”。Filter 方案就是一个强制规范它集中管理 push 和 poll不给业务代码绕过 poll 的机会。在连接池监控上建议把 Hikari 的 leak-detection-threshold 调整到 5000ms 以上避免把长查询误判为连接泄漏这样手动切换和连接池监控就能形成完整的闭环。本文还有配套的精品资源点击获取