ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot多数据源切换实战:dynamic-datasource配置与@DS事务避坑

SpringBoot多数据源切换实战:dynamic-datasource配置与@DS事务避坑 简介基于Spring Boot与dynamic-datasource的多数据源手动切换示例代码包面向需要同时接入MySQL与SQL Server的Java后端开发人员用于解决传统静态数据源配置在多数据库场景下的拆分与切换难题。压缩包共23个文件其中Java源码17个承载数据源注册、路由、切换等核心逻辑XML配置2个与YML配置1个负责多数据源初始化及相关Bean装配另有gitignore、iml、说明文档等辅助文件整体体积仅20KB。示例工程提供了清晰的工程结构与可运行的演示模块读者可对照配置文件阅读Java代码快速掌握dynamic-datasource在Spring Boot项目中的整合方式并理解手动切换数据源时路由策略的编写思路。对于需要维护多套数据库连接、在业务中动态决定使用哪个数据源的场景这份代码能提供直观的参考范式。目前已有552人学习该示例代码适合具有Spring Boot基础、希望扩充多数据源开发技能的开发者参考。1. 手动切换多数据源是什么场景以及为什么不能只配一个 DataSource一套 SpringBoot 服务里同时要查业务库和历史库业务库在 MySQL老系统在 SQLServer这是做数据迁移、报表汇总、跨系统对账时最常见的诉求。dynamic-datasource 这个库做的事本质上是把 Spring 的AbstractRoutingDataSource包装了一层运行的时候通过一个 ThreadLocal 里的 key决定下一次拿连接时用哪个数据源。但“路由”本身只是解决了“拿哪个连接”的问题真正让人栽跟头的是三件事线程里的 key 什么时候清、事务把连接绑定到线程之后还切不切得动、以及 SQLServer 的驱动和 url 写错一个参数整个服务起不来。标题里的 msyql 应该是 mysql 的笔误下文统一按 mysql 处理。这篇文章会把 dynamic-datasource 的路由模型、springboot 配置里的关键参数、DS 注解和编程式切换两种手动切换方式、以及事务场景下的坑一次讲透最后给出三种验证切换是否生效的办法。2. dynamic-datasource 的路由原理与 springboot 配置里的 4 个必懂参数2.1 为什么是它而不是自己写 AbstractRoutingDataSource自己手工实现多数据源并不复杂继承AbstractRoutingDataSource把所有的DataSource放进一个 Map重写determineCurrentLookupKey()从ThreadLocal里取出当前要用的 keySpring 每次getConnection()时都会调用这个方法。这套机制本身没有问题真正烦人的是配套代码你需要在每次请求开始时往 ThreadLocal 里塞 key、结束时清掉遇到嵌套调用还得自己维护一个栈结构否则内层切换完回不到外层的数据源再加上事务、MyBatis 插件、连接池监控这些代码一旦写起来就没完没了。dynamic-datasource 的价值在于把这些配套逻辑做成了通用能力。它的核心数据结构不是单个值而是栈push一个 key 表示切到某个数据源poll表示切回上一层这样 A 方法切到 sqlserver、调用 B 方法时又切到另一个库B 执行完 poll 之后依然回到 sqlserver不会串。同时它提供DS注解让你声明式切换支持在 Service、Mapper 甚至方法粒度上指定数据源还兼容 seata 分布式事务。对于“一个服务里读两个库”这种场景用它比自己维护一套路由代码划算得多。2.2 pom 依赖与 springboot 配置里的 4 个参数引入依赖时只需要一个 starter它会把动态数据源的核心逻辑和DS注解全部带进来。版本用 3.5.x 即可去 mvnrepository 看一眼当前最新版本。MySQL 和 SQLServer 的驱动不用单独指定版本让 spring-boot-dependencies 统一管理即可。dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.5.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.microsoft.sqlserver/groupId artifactIdmssql-jdbc/artifactId scoperuntime/scope /dependency依赖引入后spring.datasource.dynamic下面有 4 个参数需要理解清楚它们直接决定了服务启动行为和数据源选择的兜底逻辑。参数默认值作用适用场景spring.datasource.dynamic.primarymaster当没有指定数据源 key 时默认走这个数据源必须有一个明确的默认库通常就是主业务库spring.datasource.dynamic.strictfalse为 true 时指定了一个不存在的 key 会直接抛异常防止手滑把数据源名写错线上建议开spring.datasource.dynamic.strategy空自定义路由策略默认是根据 key 直接查 Map需要灰度、按条件路由时才用spring.datasource.dynamic.continue-on-errorfalse某个数据源初始化失败时是否继续启动服务测试环境数据源可能不在线时有用生产不建议开2.3 驱动与 url 写法mysql 和 sqlserver 各一例配置多数据源时最容易报错的是驱动类名和 url 格式。MySQL 8.x 必须用com.mysql.cj.jdbc.Driverurl 里要带serverTimezone否则 JDBC 驱动会拿 JVM 默认时区去连时间字段经常差 8 小时。SQLServer 用微软官方驱动url 格式和 MySQL 完全不同分号分隔参数而不是这一点务必留意。spring: datasource: dynamic: primary: master strict: true datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.1.10:3306/business_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: mysql_password sqlserver: driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://192.168.1.20:1433;DatabaseNamehistory_db;encryptfalse;trustServerCertificatetrue username: sa password: sqlserver_passwordSQLServer 的连接串里DatabaseName必须写在分号之间且encryptfalse在低版本驱动下如果不写会报 SSL 证书相关错误。如果你的 SQLServer 是命名实例而不是默认实例host 部分要写成192.168.1.20\\实例名端口则按 SQLServer 配置管理器里 TCP/IP 设置的端口来默认是 1433。还有一类老项目用的是 jtds 驱动url 前缀是jdbc:jtds:sqlserver://功能上没问题但官方驱动对高版本 SQLServer 的支持更好新项目建议直接上mssql-jdbc。3. 最小可运行示例从 pom.xml 到 DS 手动切换完整代码3.1 工程结构与依赖坐标工程结构走标准的 SpringBoot 分层Controller 暴露接口Service 里放DS注解Mapper 是 MyBatis 的接口。手动切换的意思是同一个 Service 里有两个方法一个用DS(master)走 MySQL另一个用DS(sqlserver)走 SQLServer调用方决定调哪个方法就相当于手动选择了数据源。下面是一份完整的示例代码可以直接跑。RestController RequestMapping(/api/order) public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService orderService; } GetMapping(/mysql) public Object fromMysql() { return orderService.queryFromMysql(); } GetMapping(/sqlserver) public Object fromSqlserver() { return orderService.queryFromSqlserver(); } }3.2 Service 层用 DS 做手动切换Service public class OrderService { private final OrderMapper orderMapper; public OrderService(OrderMapper orderMapper) { this.orderMapper orderMapper; } DS(master) public MapString, Object queryFromMysql() { // 注解在方法上只对当前方法生效 MapString, Object result new HashMap(); result.put(db, mysql); result.put(count, orderMapper.countFromMysql()); return result; } DS(sqlserver) public MapString, Object queryFromSqlserver() { // 同一个 Mapper同一个方法签名路由到 sqlserver 数据源 MapString, Object result new HashMap(); result.put(db, sqlserver); result.put(count, orderMapper.countFromSqlserver()); return result; } }DS注解可以放在类上表示类里所有方法默认走某个数据源放在方法上则覆盖类级别的配置。常见做法是在 Service 实现类上放DS(master)然后对需要走别的方法单独标注这样大部分请求默认走主库只有少数历史查询才切到 SQLServer。DS也支持写在 Mapper 接口上但建议优先放 Service因为切换的语义更接近业务。3.3 Mapper 与 SQL 示例Mapper public interface OrderMapper { Select(SELECT COUNT(*) FROM t_order) long countFromMysql(); Select(SELECT COUNT(*) FROM t_history_order) long countFromSqlserver(); }这里刻意让两个 Mapper 方法读不同的表t_order在 MySQLt_history_order在 SQLServer。如果你两边的表结构完全一致也可以只写一个方法countAll()让DS决定连哪个库。但要注意SQLServer 的 SQL 语法和 MySQL 有差异比如分页的写法、字符串拼接的函数、IFNULL和ISNULL的区别。如果同一个方法要兼容两个库SQL 就得写成两边都支持的方言否则建议每个库单独写 SQL。上面代码里DS(master)和DS(sqlserver)里的 key 必须和application.yml里spring.datasource.dynamic.datasource下的 key 完全一致配置里我写了strict: truekey 写错会直接抛异常启动时就能发现。3.4 手动切换的第一次验证启动服务后先用 curl 打两个接口验证路由是否生效。注意观察返回里的db字段和count数字它们分别来自两个物理库。curl http://localhost:8080/api/order/mysql curl http://localhost:8080/api/order/sqlserver如果 mysql 接口返回正常、sqlserver 接口报错不要急着怀疑 SpringBoot 版本太高这类问题先看控制台的完整堆栈。SQLServer 连接失败最常见的是驱动版本与数据库版本不匹配、TCP/IP 协议没启用、端口被防火墙挡住。如果两个接口返回的db字段都变成了 master说明DS注解没有生效先去检查是不是把动态数据源的 starter 漏掉了或者切面没有被 Spring 扫描到。DS是通过 AOP 实现的如果类上没有 Spring 的代理比如同一个类里this调用另一个DS方法注解不会触发这个坑在第 4 章展开说。4. 手动切换的三种写法与两个必踩的坑4.1 注解式DS 的 spel 表达式与动态传参DS注解支持硬编码数据源名也支持 SpEL 表达式运行时根据入参决定切到哪个库。这个能力在“按租户分库”的场景里特别有用比如请求参数里带一个tenantId不同的租户落在不同的数据库上。Service public class TenantOrderService { DS(#dataSourceKey) public ListOrder query(String dataSourceKey, Long tenantId) { // dataSourceKey 是 db_tenant_001 这样的字符串 // SpEL 表达式会在方法执行前解析取参数里的值作为数据源 key return orderMapper.selectByTenant(tenantId); } }参数dataSourceKey可以是方法入参、可以是 Spring 管理的 bean 属性也可以是一个静态方法的返回值。表达式写法是标准的 SpEL比如DS(#args[0])取第一个参数DS(dataSourceRouter.getKey())调用某个 bean 的方法。需要注意SpEL 表达式解析失败时会抛异常且这个异常在 AOP 切面里抛出和业务异常在同一个调用栈里排查时容易误以为是数据源不存在的问题。如果 key 是运行时才能确定的建议配合strict: true这样拼写错误会立刻暴露。4.2 编程式DynamicDataSourceContextHolder 的 push 与 poll注解式适合“方法级别固定切换”但有些场景是方法内部先查一次主库根据查询结果决定下一步连哪个库此时注解帮不上忙需要用编程式切换。dynamic-datasource 暴露了DynamicDataSourceContextHolder类核心方法是push、poll、peek和clear。Service public class SwitchService { public void switchByRuntime(String targetKey) { // push 是入栈操作线程当前的 key 变成了 targetKey DynamicDataSourceContextHolder.push(targetKey); try { // 这里的任何 DAO 调用都会走 targetKey 对应的数据源 ListOrder orders orderMapper.selectAll(); // 业务处理... } finally { // poll 是出栈回到 push 之前的数据源 DynamicDataSourceContextHolder.poll(); } } }push与poll必须成对出现哪怕中间的代码抛了异常poll也要在finally里执行。peek只查看当前 key 是什么不改变栈内容常用于打印日志确认当前路由。clear是清空整个栈正常情况下不用调因为一旦清空本次请求后续的所有数据库访问都会走primary指定的默认数据源相当于破坏上下文。编程式切换和DS注解是可以混用的先 push 一个 key再调用一个标注了DS的方法此时DS注解的值会 push 到栈顶方法结束后 poll 回到你手动 push 的 key再 poll 才回到最开始的默认值。理解这个栈模型嵌套切换就不会乱。4.3 坑一嵌套切换忘记 poll 导致串库假设一个请求 A 在 service 方法里 push 到 sqlserver执行完忘记 poll如果这个方法运行在 Tomcat 的线程池线程里线程归还给容器时 ThreadLocal 里的值还留着。下一个请求 B 恰好被同一个线程处理B 没有显式指定数据源按默认应该走 master结果发现查出来的数据来自 sqlserver而且这种故障是间歇性的只在并发高的时候偶发极难排查。解决办法就是上面的 try-finally 模式finally里必须poll。如果整个请求都明确知道要用哪个库也可以用DS替代手动 push让切面在方法结束后自动清理。4.4 坑二Transactional 事务方法里的切换不生效这是多数据源使用中最隐蔽的问题。Spring 的事务是基于 DataSource 的Transactional开启时Spring 会从当前数据源拿到一个连接并绑定到当前线程事务内的所有 DAO 操作都复用这个连接。dynamic-datasource 的DS即使切换了 key事务内的数据访问也不会重新获取连接而是继续用事务开启时拿到的那个连接。表现是方法标注了DS(sqlserver)也在事务里代码不报错但查出来的数据还是 master 库的。Transactional(rollbackFor Exception.class) DS(sqlserver) public void badExample() { // sqlserver 的查询不会生效连接还是 master 的 orderMapper.selectAll(); }解决方式有三种。最简单的是把DS和Transactional拆开事务方法里不直接写数据库访问而是调用一个非事务的方法去查 sqlserver让查询在事务外先完成再把结果传进事务做处理。第二种是用 dynamic-datasource 自带的DSTransactional注解替代Transactional它内部通过DataSourceTransactionManager的适配来支持动态数据源切换但需要额外引入 seata 相关依赖对事务一致性要求高才值得上。第三种是把查询 sqlserver 的操作放到REQUIRES_NEW传播级别的事务里新开一个事务让它重新走路由逻辑。日常开发中第一种最常见因为它对现有代码侵入最小。另外注意DS在同一个类的内部方法调用时不生效因为this调用不会经过 Spring 代理这是 AOP 的通用限制。5. 验证切换是否生效的三个办法与 sqlserver 侧的几个排错点5.1 打开 debug 日志确认路由结果dynamic-datasource 在每次切换数据源时都会打印一行日志你可以直接看日志确认到底路由到了哪个库。在application.yml里把相关包的日志级别调到 debug 即可。logging: level: com.baomidou.dynamic.datasource: debug日志里会出现类似dynamic-datasource switch to the datasource [sqlserver]的记录每条记录对应一次路由切换。如果日志里显示切到了 sqlserver 但数据不对问题就不在路由而在你的 SQL 或者事务。5.2 用数据库函数确认当前连接在哪个库两边表结构完全一致时返回的行数无法区分到底是哪个库的数据。用数据库自带的函数直接返回库名是最直观的验证方式。-- mysql SELECT DATABASE() AS current_db; -- sqlserver SELECT DB_NAME() AS current_db;把这两个 SQL 分别写进两个 mapper 方法接口返回current_db字段。如果返回的是business_db和history_db说明路由正确。这种方法在测试环境排查问题时比看日志更可靠因为返回值是你直接拿到的结果不受日志缓存影响。5.3 sqlserver 连接不上的三个检查点SQLServer 和 MySQL 不同安装后默认可能没开 TCP/IP 协议。连不上时先检查三点打开 SQLServer 配置管理器确认 TCP/IP 协议是“已启用”状态确认端口是 1433或你自定义的端口且 IP 地址栏里允许的 IP 段包含客户端所在网段用telnet 数据库IP 1433测一下端口通不通。如果本机能连、远程连不上大概率是 Windows 防火墙或 SQLServer 的 TCP 端口配置问题跟 SpringBoot 代码无关。还有一点SQLServer 2022 的安装教程网上很多装的时候如果要让远程访问别忘了在 SQL Server 配置管理器里把客户端协议的 TCP/IP 也打开否则服务端开着、客户端连不上排错会绕很大一圈。5.4 一个推荐的生产排查技巧响应头里带上数据源名前面介绍的验证方式都依赖手动查询结果或日志生产环境里最实用的是把当前请求命中的数据源名写进响应头。写一个拦截器从DynamicDataSourceContextHolder.peek()取出当前 key放进 response header。Component public class DataSourceHeaderInterceptor implements HandlerInterceptor { Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { String dataSource DynamicDataSourceContextHolder.peek(); if (dataSource ! null) { response.setHeader(X-DS-Name, dataSource); } } }前端或排查问题时用 curl 加-i参数看一眼响应头就知道这次请求实际走的是哪个库。peek不会改变栈内容放心在拦截器里用。这个头在生产环境长期保留也没问题对排查串库、走错库这类问题非常有价值。本文还有配套的精品资源点击获取
返回列表