ARTICLE DETAIL

资讯详情

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

dynamic-datasource报错can not find primary datasource的排查与修复

dynamic-datasource报错can not find primary datasource的排查与修复 一个项目跑了大半年都好好的某天发布新版本之后控制台突然被一条报错刷屏关键信息就一句dynamic-datasource can not find primary datasource。别慌这类问题我前前后后遇到过不少次多数情况不是代码逻辑变了而是数据源配置、依赖引入或者自动装配链路出了问题。这个报错的字面意思是“找不到主数据源”但真正的原因往往藏在主数据源是怎么被识别、加载和注册的这个过程里。这篇文章就把我排查这个报错的完整思路和修复方案拆开来讲给遇到同样问题的同学一条能直接照着走的排查路径。1. 报错源头DynamicRoutingDataSource 的路由机制和 primary 的兜底逻辑1.1 报错信息是从哪个环节抛出来的dynamic-datasource 是 MyBatis 生态里很常用的多数据源组件底层核心类叫DynamicRoutingDataSource它继承自 Spring 的AbstractRoutingDataSource。Spring 的抽象路由数据源有个关键方法determineCurrentLookupKey()这个方法的作用是决定“当前这次数据库操作该用哪个数据源”。dynamic-datasource 对这个方法的增强逻辑大致是Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.peek(); }代码不长但背后有一套完整的路由决策过程项目里有很多数据源启动时按配置加载进一个MapString, DataSource这个 Map 的 key 就是你在配置里给每个数据源起的名字常见叫master、slave、order_db这类。每次执行数据库操作前DynamicDataSourceContextHolder里会有一个当前线程绑定的数据源 key这个 key 的来源可能是DS(order_db)注解、手动调DynamicDataSourceContextHolder.push()或者没有指定策略时走默认兜底。拿到 key 之后DynamicRoutingDataSource去 Map 里找对应的 DataSource。如果没找到就再看strict模式的配置。如果strict: false默认值找不到指定 key 时会回退到 primary 指定的数据源。如果连 primary 指定的数据源都找不到就会抛出CannotFindDataSourceException也就是日志里那句can not find primary datasource。所以这个报错本质上是在说你要找的数据源没找到而我打算兜底的主数据源也没找到。1.2 primary 的定位机制和常见误解primary在 dynamic-datasource 里是通过spring.datasource.dynamic.primary这个配置项指定的默认值是master。也就是说即使你没有显式写primary: master组件也会默认去找一个 key 为master的数据源充当兜底主数据源。很多项目出问题就在这一步配置文件里明明配了一个数据源但 key 不叫master也没显式指定 primary。结果启动能起来一旦某个请求没匹配到指定数据源组件就尝试回退到 master然后发现压根没有这个 key立刻抛异常。还有一层容易忽略的细节是primary 只是注册在配置里的字符串它不会帮你在数据源 map 里主动创建数据源。只有spring.datasource.dynamic.datasource下面真实配置了对应 key 的数据源primary 才有意义。换句话说primary 是一把“钥匙”但门DataSource 实例得你自己配好。我用一个不太严谨但很直观的比喻动态数据源就像公司前台来电没有指定找谁时默认转给店长。结果店长这个岗位压根没人入职那所有转过去的电话都得断。排查的关键就是先确认“店长到底有没有入职”。2. 最容易触发这个报错的五种配置场景这个报错不是某一类单一原因导致的把常见触发场景摆出来你可以直接对号入座。2.1 场景一名下只有一个数据源但 key 不叫 master最经典的情况spring: datasource: dynamic: datasource: db_core: url: jdbc:mysql://localhost:3306/core username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver看起来没毛病启动过程一般也不报错。但此时 primary 默认是master数据源 map 里却只有db_core。一旦有查询没有用DS指定数据源或者指定了一个不存在的 key 且 strict 为 false组件就会兜底到 master然后当场报找不到 primary。真实项目里我见过最离谱的版本是primary指向Master首字母大写数据源 key 是master。字符串比较是大小写敏感的结果是同样的报错。这也提醒我们配置 primary 时最好和数据源 key 完全复制粘贴别手打。2.2 场景二主从或多库配置里primary 指了一个没被加载的数据源多数据源配置下更容易踩坑spring: datasource: dynamic: primary: order_db # 想要默认走订单库 datasource: user_db: url: jdbc:mysql://localhost:3306/user username: root password: root order_db: url: jdbc:mysql://localhost:3306/order username: root password: root这种配置本身没问题。但假如某天有一台机器上 order_db 的数据库连接串因为环境变量注入失败变成了空字符串或者配置中心下发时把 order_db 这段整个覆盖掉了数据源 map 里就只剩 user_db。primary 指向 order_db 却找不到对应实例报错同样会出现。这种情况最坑的是启动时不一定会立刻报错数据库连接池很多是懒加载的等第一次请求进来才初始化连接报错往往发生在线上流量进来之后而不是发布的时候。2.3 场景三自定义 DataSource 配置把动态数据源挤掉了这是排查中经常被忽略的一种。项目里如果存在这样一个配置类Configuration public class DataSourceConfig { Bean Primary public DataSource dataSource() { return DruidDataSourceBuilder.create().build(); } }而且这个 Bean 的返回类型是DataSource那问题就来了。Spring Boot 的自动配置通常依赖ConditionalOnMissingBean(DataSource.class)这样的条件注解。一旦你自己注册了一个 DataSource 类型的 Bean动态数据源的自动配置就会被判定为“已经有数据源了不需要我再创建一个”整个DynamicRoutingDataSource就不会被装配。此时项目用的就是你自定义的那个裸数据源dynamic-datasource 的动态路由完全没启动结果就是调用链上还在用DS或者动态路由组件自然找不到 primary。这种场景下报错信息可能是启动后立刻出现也可能是运行时某个代理对象初始化时出现取决于项目里其他组件对 DataSource 的使用方式。2.4 场景四依赖引错了starter 根本没生效dynamic-datasource 的依赖分几类dynamic-datasource-spring-boot-starter完整 starter会自动装配。dynamic-datasource-spring-boot3-starterSpring Boot 3.x 专用。dynamic-datasource-core只提供核心能力不会自动装配。有些项目是 common 基础模块里引入了 core业务模块又只依赖 common没单独引 starter。结果业务代码里DS注解能用因为 core 包里也有注解类但自动装配完全没有执行DynamicRoutingDataSource根本没有实例到了运行期一样报 can not find primary datasource。还有一种情况是多个模块各自引入了不同版本的 dynamic-datasource最后 classpath 里存在多个版本的类。特别是一些中间件或框架为了自己的功能也会传递引入这个组件的旧版本版本号冲突会让自动装配逻辑走到错误的分支。2.5 场景五Spring Boot 3.x 用了不支持自动装配的老版本 starterSpring Boot 的自动装配注册机制有过一次重要调整从 Spring Boot 2.7 开始新增了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这个注册文件到了 Spring Boot 3.x老的spring.factories里写自动配置的方式默认不再生效。如果项目升级到 Spring Boot 3但仍然依赖的是 dynamic-datasource 老版本比如 3.4.0 之类的自动配置类很可能根本不会被加载。表现就是启动时没有报错但数据源没有被初始化等实际使用数据源时抛出一堆异常其中就包含 primary not found。要判断是不是这个原因最直接的方式是看是否使用了dynamic-datasource-spring-boot3-starter或者说版本是否在 3.5.0 及之后。版本选型这件事后面我会专门给一套推荐组合。3. 完整排查链路从启动日志到运行时状态报错出现之后别急着改配置文件。下面这条链路我每次都会完整走一遍基本能定位 90% 的问题。3.1 第一步盯住启动日志里的数据源初始化痕迹dynamic-datasource 启动正常时会打印类似这样的日志dynamic-datasource initial load [master, slave_1] dynamic-datasource enabled注意这行日志。它表示启动时成功加载了哪些数据源 key。如果没有打印这行日志说明自动装配没执行如果打印了但 key 列表里没有 primary 指定的那个说明配置加载了但 key 对不上。线上环境日志量大的时候可以直接把日志级别调高或者用 grep 过滤grep dynamic-datasource app.log | head -20如果连dynamic-datasource enabled都没看到基本可以确认是自动配置失败重点检查依赖和条件装配。3.2 第二步用代码看一眼 DynamicRoutingDataSource 里到底有什么日志不可靠的情况下直接注入组件看运行时状态是最稳的。可以写一个临时的ApplicationRunnerComponent public class DataSourceCheckRunner implements ApplicationRunner { Autowired private DynamicRoutingDataSource dynamicRoutingDataSource; Override public void run(ApplicationArguments args) { MapString, DataSource dataSources dynamicRoutingDataSource.getCurrentDataSources(); System.out.println(Current datasource keys: dataSources.keySet()); System.out.println(Primary: dynamicRoutingDataSource.primary()); } }这个探针会打印出运行时真正被加载的数据源 key 集合以及 primary 指向的 key。对照一下就能发现是“primary 指向不存在”还是“数据源根本没加载”。如果你的项目里连DynamicRoutingDataSource都注入不进去说明动态数据源压根没被创建问题在装配层面不在配置值层面。3.3 第三步核对配置文件加载路径与最终生效配置很多时候配置值本身没问题但“最终生效的值”有问题。常见情况有多个环境共用一套配置application-dev.yml覆盖了application.yml里的 primary 值。Nacos 或 Apollo 配置中心里配置覆盖了本地配置。同一个 key 在不同配置文件里重复定义YAML 解析后取到了意外值。用 Spring Boot Actuator 的 env 端点快速确认curl http://localhost:8080/actuator/env/spring.datasource.dynamic.primary如果返回的propertySources里有多个来源逐个对比定位是哪个配置源覆盖了 primary 值。如果环境变量注入的方式不对也会导致 URL 或用户名变空。比如url: ${DB_URL}即便环境变量没注入Spring 启动时一般会报占位符解析失败但有些情况比如在配置中心里设了空值会直接注入空字符串连接池创建时默认是能创建的等连接时才发现不行表现就和 primary not found 完美混淆。3.4 第四步查依赖树确认版本是否统一依赖问题的排查Maven 项目直接看依赖树mvn dependency:tree -Dincludescom.baomidou:dynamic-datasource-spring-boot-starter mvn dependency:tree -Dincludescom.baomidou:dynamic-datasource-core重点看两件事classpath 里是否存在多个版本的 dynamic-datasource。是引了 starter 还是只引了 core。Gradle 项目对应用gradle dependencyInsight --dependency dynamic-datasource看到多个版本的时候优先用 dependencyManagement 或者直接统一到明确版本避免“被中间件传递引入的老版本把自动装配逻辑带偏”。4. 修复实操配置、依赖、代码三层对齐排查完修复起来其实都是对症下药。我按三个层面来梳理。4.1 配置层一份能稳定运行的最小配置最基本的一条原则是primary 指向的 key 必须在 datasource 列表里真实存在。推荐的做法是显式写明 primary不依赖默认值并且把 key 命名为容易识别的master。一份完整的最小配置如下spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://localhost:3306/main_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver report_db: url: jdbc:mysql://localhost:3306/report_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver这里master和report_db是数据源 keyprimary: master保证默认走主库。如果配合 Druid 连接池可以增加spring: datasource: dynamic: druid: initial-size: 5 max-active: 20 min-idle: 5这种写法是给所有动态数据源统一设置 Druid 参数避免每个数据源重复写一遍。如果你想让某个数据源使用不同的连接池比如 HikariCP可以用spring: datasource: dynamic: datasource: master: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://...4.2 依赖层Spring Boot 和 dynamic-datasource 的版本匹配版本选型非常关键。根据我的实际使用经验推荐组合如下Spring Boot 版本dynamic-datasource 依赖推荐版本Spring Boot 2.xdynamic-datasource-spring-boot-starter3.5.2 及以上Spring Boot 3.xdynamic-datasource-spring-boot3-starter4.1.3 及以上如果你的项目卡在 Spring Boot 2.x至少用 3.5.0因为 3.5.0 开始适配了新版自动装配注册机制配合 Spring Boot 2.7 没有兼容性问题。如果你的项目已经上了 Spring Boot 3还在用 3.x 版本的 starter大概率就是自动装配没生效。直接换成dynamic-datasource-spring-boot3-starter很多莫名奇妙的问题会消失。完整坐标示例!-- Spring Boot 3.x -- dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot3-starter/artifactId version4.1.3/version /dependency!-- Spring Boot 2.x -- dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.5.2/version /dependency4.3 代码层数据源全权交给 dynamic-datasource别手动注册裸 DataSource修复过程中最核心的一个代码动作就是确保项目里没有手动注册一个覆盖性质的 DataSource Bean。如果你确实需要在业务代码里创建数据源正确的做法是把这些数据源配置到 dynamic-datasource 的 datasource 列表里让它统一管理而不是自己在配置类里 new 一个 DataSource 返回。dynamic-datasource 设计的初衷就是让数据源的创建、路由、健康检查都走组件本身。如果你一定要保留自定义 DataSource比如某些特殊连接池参数无法通过配置文件表达至少要把动态数据源的创建逻辑也一并保留。一个可供参考的写法是Configuration public class DataSourceConfig { Bean ConfigurationProperties(spring.datasource.dynamic.datasource.master) public DataSourceProperty masterDataSourceProperty() { return new DataSourceProperty(); } Bean Primary public DataSource dynamicDataSource() { DynamicRoutingDataSource dataSource new DynamicRoutingDataSource(); dataSource.setPrimary(master); dataSource.setStrict(false); return dataSource; } }但这套写法需要你手动把数据源塞进 DynamicRoutingDataSource配置和代码耦合度很高我不推荐日常使用。除非你有非常特殊的需求否则把数据源管理权完全交给 dynamic-datasource是省心且正确的路。5. 修好之后验证清单和减少复发的日常习惯5.1 验证清单确认动态数据源真的恢复了修复完成后逐一确认以下四点启动日志出现数据源加载列表能看到dynamic-datasource initial load [master, report_db]列表里的 key 符合预期。无 DS 路径能正常查询主库写一个不指定数据源的接口确认默认走 master。有 DS 路径能切到目标库写一个DS(report_db)的接口确认能查到对应库的数据或者通过数据库连接日志确认切换成功。异常 key 不导致系统崩溃如果strict: false访问一个不存在的 key 时会自动回退到 primary 而不抛异常。验证时可以看运行时日志dynamic-datasource 在切换数据源时会打印类似dynamic-datasource switch to the datasource: report_db能稳定打印这条日志说明动态路由恢复了。5.2 减少复发的几个日常习惯几次踩坑之后我总结了一些经验能明显降低这个报错的概率数据源 key 的命名规则要和 primary 配置解耦。哪怕只有一个数据源也建议 key 命名为master并显式写primary: master不给默认值留隐患。多个环境的配置统一管理。一个很好的做法是main 分支的配置文件里就固定好 primary 和所有数据源 key测试环境、生产环境只替换 URL、账号、密码不改 key 名。依赖升级前先查版本兼容矩阵。dynamic-datasource 这类组件对 Spring Boot 大版本很敏感升级前先看一眼官方文档或 Maven 仓库里的依赖声明。启动探针里带数据源自检。我习惯在服务启动后加一个检查调用一次DynamicRoutingDataSource.getCurrentDataSources()如果 primary 不在 key 集合里直接启动失败而不是等线上请求来触发报错。下面这个自检工具类可以直接用Component public class DynamicDataSourceStartupValidator implements ApplicationRunner { Autowired private DynamicRoutingDataSource dynamicRoutingDataSource; Override public void run(ApplicationArguments args) { MapString, DataSource dataSources dynamicRoutingDataSource.getCurrentDataSources(); String primary dynamicRoutingDataSource.primary(); if (!dataSources.containsKey(primary)) { throw new IllegalStateException( Dynamic datasource primary validation failed: primary primary , available keys dataSources.keySet() ); } } }这段代码把问题暴露时机从“第一次请求”提前到了“应用启动”代价很小收益很大。我自己把这段逻辑固化在基础工程里之后再也没在线上被这个报错突袭过。
返回列表