首页 / 资讯中心 / 文章详情

若依微服务多数据源改造:MySQL与达梦DM双库切换实操指南

若依微服务多数据源改造:MySQL与达梦DM双库切换实操指南 ★ FEATURED ARTICLE
项目标题里同时出现“若依微服务”和“MySQL DM”说明你大概率正在做一件很常见的事把一个原本只连 MySQL 的若依微服务项目改造成能同时访问 MySQL 和达梦数据库DM的双数据源工程。而且这个“多数据源”不是DEMO里那种糊弄人的玩具配置是真要落到业务上、能稳定切换查询的那种。这段时间我刚好把一个若依微服务模块从“单 MySQL”改造成“MySQL 主库 DM 从库”中间翻过不少资料也踩了几个不太容易发现的坑。更难受的是若依本身版本多、模块杂网上文章东一句西一句很难一口气照着做成功。所以我把这次完整的过程沉淀成一篇能直接照着操作的实操笔记从架构思路、依赖改造、Nacos 配置到业务代码里的DS注解切换、事务边界、分页方言以及最后的高频排错全部串起来讲清楚。如果你正卡在“驱动装不上”“切换不生效”“分页 SQL 在 DM 上报错”这几个典型环节这篇文章应该能直接给你答案。1. 项目背景与多数据源需求拆解1.1 为什么会在若依微服务项目中遇到多数据源很多团队最初选若依就是看中它开箱即用Nacos 做注册配置中心各微服务按业务边界拆分system管权限、job管定时任务、auth管认证数据层统一走 MySQL。这套架构在纯新建系统里很舒服但一旦涉及数据迁移、系统对接、历史系统改造问题就来了。我这次遇到的实际场景是新平台用若依微服务跑主业务数据在 MySQL 里但一个老项目的数据仍然留在达梦数据库 DM8 中。而且老项目不会很快下线新平台实时业务需要频繁读取 DM 里的存量数据。最直接的想法就是把老库直接迁到 MySQL但业务方不同意因为老系统还在持续写入双写方案又太复杂。于是退而求其次让若依的某个微服务同时持有两个数据源默认走 MySQL需要查老数据时切到 DM。这种情况在政务、军工、金融类的国产化替代项目里特别典型。达梦数据库本身兼容 Oracle 模式很多老系统从 Oracle 迁到 DM 后仍然跑得好好的而新开发的业务系统往往选 MySQL 生态两边共存是常态。所以与其说是“一个服务连两个库”不如说是在一套微服务框架里维护两套不同数据库生态的访问能力。1.2 两条主流实现路线多套会话工厂与动态数据源第一次接触多数据源的人最容易在网上搜到两类方案这里先说清楚它们的本质区别避免走弯路。第一类给每个数据源单独创建一套SqlSessionFactory和TransactionManager。也就是说一个服务里同时存在两套 MyBatis 配置每套对应自己的DataSource业务代码里必须明确指定用哪一套。好处是很“正统”坏处是配置量巨大每个 Mapper 都要确认绑定到哪个工厂事务边界也容易乱。对于若依这种模块化很深的框架这种方案侵入性太强改起来烦得很。第二类用动态数据源框架比如dynamic-datasource-spring-boot-starter把所有数据源交给一个路由管理器统一持有。运行时通过 AOP 拦截带有DS(xxx)的方法动态把当前线程绑定的DataSource切到指定数据源方法执行完再自动切回来。这个思路本质上是把“多数据源”抽象成一个“数据源路由表”。我最后选的就是第二类。原因是若依多个分支版本里其实已经内置了 dynamic-datasource 的依赖或者至少预留了类似的结构改动最小、最容易落地。而且它的DS注解是方法级的在 Service 层一切换就能隔离不需要动 Mapper XML。1.3 为什么最终选择动态数据源方案除了侵入性小还有一个很现实的原因统一连接池管理。若依默认配 Druiddynamic-datasource 对它支持得比较好可以给每个数据源单独配置连接池参数。比如 MySQL 主库并发高连接数给大点DM 老库只是偶尔查报表连接数给小点互不影响。这在多数据源场景下非常重要因为两个库的连接池如果完全搅在一起一个库出问题会把另一个也拖垮。另外dynamic-datasource 支持primary默认数据源设置。也就是说就算某个业务方法忘了加DS它会老老实实走主数据源不会因为没注明路由就报错。这个特性对老模块迁移特别友好可以先不动的代码新写的功能再逐步切其他数据源。2. 环境准备与依赖改造2.1 版本组合建议多数据源成功的关键首先在版本匹配。我这里强烈建议先确认你的若依微服务版本再动手不同版本的若依封装的依赖差别很大网上配置抄错一点就是一堆诡异报错。如果你用的是当前主流的若依微服务版本大致对应 Spring Cloud Alibaba 体系常见的组合是组件推荐版本Spring Boot2.5.x 或 2.6.xSpring Cloud2020.0.x 或 2021.0.xNacos2.xMyBatis3.5.x 以上若依自带MySQL 驱动mysql-connector-java 8.0.x达梦 JDBC 驱动DmJdbcDriver18 或 DM8 自带版本需根据数据库小版本确认dynamic-datasource3.5.x 及以上有两点要特别提醒第一达梦 JDBC 驱动的版本不是越新越好要和 DM8 服务器的版本对齐否则连接时可能出现协议不兼容第二dynamic-datasource 如果你是自己引入的注意和 Spring Boot 版本兼容3.5.x 对 Spring Boot 2.x 支持得比较稳。2.2 达梦 JDBC 驱动的获取与安装达梦数据库的 JDBC 驱动不像 MySQL 那样随便从 Maven 中央仓库拉。最常见的两种拿法一种是从达梦数据库安装目录里找路径一般是%DM_HOME%/drivers/jdbc/里面会有DmJdbcDriver18.jar或者DmJdbcDriver8.jar。根据服务端版本选合适的拷到本地。另一种是通过 Maven 安装到本地仓库。如果你公司内部有私有 Maven 仓库还可以把驱动上传上去方便多个模块引用。命令大致是mvn install:install-file -DfileDmJdbcDriver18.jar -DgroupIdcom.dameng -DartifactIdDmJdbcDriver18 -Dversion18.x -Dpackagingjar安装完成之后在pom.xml里声明依赖dependency groupIdcom.dameng/groupId artifactIdDmJdbcDriver18/artifactId version18.x/version /dependency如果你用私有仓库安装命令换成mvn deploy:deploy-file上传到远程仓库即可。驱动类的名称要记好后面配置里要用dm.jdbc.driver.DmDriver这也是网上很多文章容易写错的点有人写dm.jdbc.driver.DmDriver有人写dm.jdbc.driver.DmDriver大小写不一致实际上这串类名是严格区分大小写的。2.3 重要模块的 Maven 依赖调整若依微服务是多模块结构依赖不能乱加。你要先明确到底哪个服务需要连 DM多数据源配置并不是所有服务都能无脑套的。一般业务系统会把数据访问集中在ruoyi-system、ruoyi-job、或自己扩展的业务服务模块。你需要给这些模块的pom.xml增加两样东西一是达梦 JDBC 驱动依赖二是确认 dynamic-datasource 相关依赖已经在ruoyi-common-datasource里存在。我这次做法的参考步骤如下打开目标服务的pom.xml它通常已经引用了ruoyi-common-datasource或ruoyi-common-mybatis。查看ruoyi-common-datasource的pom.xml确认里面是否有dynamic-datasource-spring-boot-starter的依赖。如果没找到就手动加进去dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.5.2/version /dependency在目标模块中增加达梦 JDBC 驱动dependency groupIdcom.dameng/groupId artifactIdDmJdbcDriver18/artifactId version18.x/version /dependency这里有个非常容易踩的坑有些若依项目的ruoyi-common-datasource模块默认依赖里会把旧版 dynamic-datasource 排除掉换成自定义的数据源路由。遇到这种情况需要先把自动装配类看清楚否则你就算在 Nacos 里写满配置Spring 也没有加载 dynamic-datasource 的自动配置DS注解永远不会生效。建议在改造前做一次确认直接搜索整个 common 模块下是否包含DynamicDataSourceAutoConfiguration或DS对应的DynamicAopAdvisor。找不到就补依赖找得到就继续。3. 多数据源配置实操从 Nacos 到连接池3.1 Nacos 中 dynamic 数据源树结构怎么排若依微服务里数据库连接配置一般不在本地application.yml而是统一放在 Nacos 配置中心。你要修改的是对应服务的数据源配置文件名称通常是ruoyi-system-dev.yaml或类似结构。先看原本的配置长什么样一般是spring: datasource: druid: stat: true type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root url: jdbc:mysql://localhost:3306/ry-cloud?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8改成 dynamic-datasource 结构后应该是这样spring: datasource: dynamic: primary: master strict: false datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ry-cloud?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8 username: root password: root dm: driver-class-name: dm.jdbc.driver.DmDriver url: jdbc:dm://192.168.10.25:5236?schemaDM_TEST username: SYSDBA password: SYSDBA druid: initial-size: 5 max-active: 20 min-idle: 5 max-wait: 60000注意原本的driver-class-name、url这些字段都要移动到datasource.master下面不能还留在原来的spring.datasource顶层。否则 Spring 还是会优先按单数据源去初始化导致你配置的dm根本不会加载。3.2 主从数据库配置项逐个说明上面那串配置里有几个关键概念你必须搞清楚不然配置写完了也只会照猫画虎。primary: master的含义是默认数据源。所有没有标注DS的方法最终都会路由到master这个数据源上。对于老代码来说这就是“无感兼容”的保证不用给历史 Service 全部加注解。strict: false的含义比较隐蔽。它控制的是“当你指定了一个不存在的数据源时怎么办”。false表示不报错回落到默认数据源true表示直接抛异常。我建议在调试阶段先设成true这样如果你代码里写了DS(dm)但配置里没配dm数据源会立刻暴露问题。上线前再根据情况改回false。driver-class-name和url不用多说。但达梦数据库的 URL 格式有个细节jdbc:dm://IP:PORT?schemaxxx。如果达梦数据库建的用户默认 schema 不是你要访问的必须在 URL 里明确指定 schema。我一开始没带 schema 参数能连接上但 SQL 执行时总是找不到表一度以为驱动有问题最后才发现是 schema 没指定。还有连接池参数。如果你继续用druid配置块它会作为默认共享配置套到所有数据源上。但你也可以给单个数据源单独覆盖比如datasource: dm: druid: max-active: 10这样 DM 数据源就单独用 10 个最大连接不会抢占 MySQL 的连接池资源。3.3 若依自带的自动装配能省多少事有一些若依版本已经集成了 dynamic-datasource 的自动配置甚至你不需要写任何Configuration类只需要在配置中心把数据源结构改成 dynamic 的格式即可。具体省事在哪因为dynamic-datasource-spring-boot-starter自带DataSourceAutoConfiguration的替换逻辑它会扫描spring.datasource.dynamic.datasource下的所有键为每个键创建独立的DruidDataSource并注册到路由表中。Java 代码里只需要注入DataSource时用Autowired或Resource(name dataSource)即可拿到的其实是路由代理。如果你的若依版本没有启用自动配置那就需要自己写一个配置类手动把配置项绑定到DynamicDataSource。但这种情况比较少见遇到的话优先检查是否漏了启动类扫描范围或者依赖没引全。3.4 手动切换数据源栈的正确姿势DS注解是常用的但有些场景不适合用注解。比如在一个长任务里需要先查 MySQL再查 DM再回 MySQL中间还要动态决定是查 A 还是查 B。这时候纯注解很难表达就需要手动路由。dynamic-datasource 提供了线程绑定机制DynamicDataSourceContextHolder.push(dm); try { // 此时的 dataSource 自动指向 dm dmMapper.queryList(); } finally { DynamicDataSourceContextHolder.poll(); }push是入栈poll是出栈。这个栈设计对嵌套调用很友好A 方法 push 了dm里面调用 B 方法又 push 了另一个数据源B 结束之后 poll 回dm不会串。我强烈建议在开发规范里定一个死规矩手动 push 必须和 poll 配对且 poll 要放在 finally 里。否则高并发下线程池中的线程会被上一次的数据源标记污染。这个问题极度隐蔽平时单测很难发现只有高并发压测时才会突然出现“查询结果莫名指向另一个库”的诡异现象。4. 业务代码改造与典型用法4.1 用 DS 注解做最小侵入切换DS注解是这整套方案的使用核心它可以直接加在 Service 实现类的方法上面。比如Service public class DataQueryServiceImpl implements DataQueryService { Autowired private TestDmMapper testDmMapper; Autowired private TestMysqlMapper testMysqlMapper; DS(dm) Override public ListDemoData listFromDm() { return testDmMapper.selectList(); } Override public DemoData getFromMysql(Long id) { return testMysqlMapper.selectById(id); } }这个方法执行时testDmMapper就会走 DM 数据源而且整条调用链上这个数据源会被绑定到当前线程。方法返回后数据源自动释放不会影响外层调用。有个常见疑问DS能不能加在 Mapper 接口上表面上看dynamic-datasource 支持在 Mapper 上标注DS也能生效。但我个人不建议在 Mapper 上标注因为 Mapper 会被多个 Service 复用一旦某个 Service 在事务里先查了 MySQL 再调这个 Mapper标注在 Mapper 上的DS可能会和事务产生冲突。更合理的做法是把它标注在 Service 方法上把数据源的选择提升到业务服务层这样语义更清晰。4.2 事务边界与数据源切换的执行顺序这是多数据源配置里最容易翻车的环节。先看一个反面示例Transactional(rollbackFor Exception.class) DS(dm) public void saveDmAndMysql() { dmMapper.insert(); mysqlMapper.insert(); // 这里不会自动切回 MySQL }这里有个陷阱Transactional和DS同时存在时数据源的切换必须先获取连接而 Spring 的事务管理是在进入方法时就确定了一个数据库连接。如果你的DS(dm)安在事务方法上那么这个事务内的所有 SQL 都会走 DM哪怕你代码里操作 MySQL 的 Mapper它实际上也是通过事务线程绑定的 DM 连接去执行结果要么报错要么找不到表。正确做法是分两个事务方法或者使用REQUIRES_NEW传播行为把不同数据源的操作拆到独立事务中Transactional(rollbackFor Exception.class) public void saveAll() { saveToDm(); saveToMysql(); } DS(dm) Transactional(rollbackFor Exception.class, propagation Propagation.REQUIRES_NEW) public void saveToDm() { dmMapper.insert(); } Transactional(rollbackFor Exception.class, propagation Propagation.REQUIRES_NEW) public void saveToMysql() { mysqlMapper.insert(); }简单说多数据源场景下“一个方法干多个库的活”是伪需求真正的正确做法是按照数据源拆分事务边界再在编排层组合调用。这个编排层的代码虽然多写几行却能避免大量连接错乱问题。4.3 跨库查询与方言适配实操业务上最常见的需求是主数据在 MySQL但某几个属性要从 DM 老系统里补全。这时候不推荐直接跨库 JOIN因为两个库分属不同数据库产品即使都支持标准 SQL也很难保证分页、函数、排序行为完全一致。更稳的方案是分两次查询在应用内存中做结果合并。如果两个库的数据量不大这个方案性能完全能接受。比如查订单列表先走 MySQL拿到的结果包含userId然后根据userId批量查 DM 里的用户扩展信息在 Java 里Map匹配后组装返回。另外达梦数据库兼容 Oracle 模式SQL 语法上有些地方和 MySQL 不同。比如MySQL 用LIMIT 0, 10DM 在 Oracle 兼容模式下更推荐ROWNUM或者FETCH FIRST 10 ROWS ONLY。MySQL 的IFNULLDM 里要写NVL。字符串拼接MySQL 是CONCAT(a, b)DM 兼容 Oracle 用的是a || b。日期函数差异明显NOW()在 DM 中要考虑SYSDATE或CURRENT_TIMESTAMP。所以最稳妥的方式是为 DM 单独写 SQL条件允许的话Mapper XML 文件单独建一套避免在通用 mapper 里硬写方言函数。分页也要单独说。若依用的是 PageHelper默认方言通常是 MySQL。PageHelper 对达梦的支持要看版本新版 PageHelper 已经支持 DM 方言但还是需要显式指定pagehelper: helper-dialect: oracle reasonable: true support-methods-arguments: true如果达梦库是 Oracle 兼容模式把helper-dialect配成oracle通常可行。如果仍出现分页 SQL 无法解析的情况说明你的分页方式和 PageHelper 自动检测不匹配就要考虑在 DM 查询里手动分页或者通过自定义拦截器处理。4.4 微服务模块之间的数据源职责划分若依微服务是多个独立服务通过 Nacos 互相调用。多数据源配置的粒度要按服务划分我建议不要把“一个服务连无数个数据源”当成默认设计。举个例子ruoyi-system服务主要负责用户、角色、权限它一般只需要连 MySQL。ruoyi-job服务如果涉及定时任务调度也可以连主库。而真正对接 DM 的业务模块比如你的业务服务business服务才需要引入 DM 驱动和多数据源配置。其他服务保持单数据源能显著减少错误面。服务间要查 DM 数据更推荐通过 OpenAPI 方式调用业务服务自己连 DM 后把结果封装成 JSON 接口其他服务通过 Feign 调用。而不是让所有服务都配一份 DM 数据源那样既浪费连接又难以维护。不过有一种例外情况你有一个公共服务 A 被多个服务通过 Feign 调用而这个服务内部可能需要同时访问 MySQL 和 DM。这种设计尽量把 DM 访问收敛在少数几个 Service 中多余的数据访问不应该铺到整个客户端 API 层。5. 踩坑记录与问题排查实录5.1 驱动程序找不到或连接不上的排查思路最常见的就是启动或者执行时抛出Cannot load driver class: dm.jdbc.driver.DmDriver。这种问题十有八九不是代码问题而是依赖打进了错误模块或者根本没打包进最终可执行 JAR。排查步骤确认目标服务中的pom.xml已经声明达梦驱动依赖。检查mvn dependency:tree输出确认依赖真的存在于当前模块依赖树中。查看打出的 JAR 包内容jar tf 服务名.jar | grep dm.jdbc。看不到就说明依赖没打进包检查spring-boot-maven-plugin的打包配置是否有 exclude 把该依赖排除了。确认驱动类名大小写。虽然类加载通常对大小写敏感但你在配置里写错大小写会直接导致加载失败。确认驱动版本和服务端版本匹配。DM8 的驱动通常向后兼容性还可以但如果服务端是极老版本新驱动可能连不上。我遇到过一种特别隐蔽的情况驱动确实被打进了包但ruoyi-common-datasource里面对 JDBC 驱动做了 enable 或 disable 的开关配置把某些驱动类给过滤了。这种应用级过滤在日志中往往没有任何显式报错只是静默地找不到驱动。解决办法是全局搜索driver相关配置看看是否被自定义代码影响了。5.2 DS 不生效与“切换串库”的隐蔽原因DS注解不生效但项目又能正常启动这种问题非常让人头疼。根据我实际经验原因集中在以下几个方面。首先检查方法调用是否走了代理。DS是基于 AOP 代理实现的当你在同一个类的内部方法里调用另一个带注解方法时比如public void outer() { this.inner(); // 不会走代理 } DS(dm) public void inner() { }this.inner()不会触发注解数据源不会切换。正确做法是把inner()挪到另一个类或者注入自身的 Spring 代理对象。其次DS和Transactional同时存在时上面 4.2 节已经说过顺序问题。事务一旦开启连接已经绑定到线程再切换数据源就晚了。再次检查 dynamic-datasource 自动配置是否被若依自己的数据源配置覆盖了。若依有些版本会自定义DruidConfig或DataSourceConfig如果这个配置类用Primary强制指定了某个数据源那 dynamic-datasource 的路由机制可能根本没启用。这时候需要对比启动日志中加载的DataSource类型如果看到的是DruidDataSource而不是DynamicRoutingDataSource那说明自动配置被挤掉了。解决办法是在启动类上或者配置类中显式排除若依原有的数据源自动配置SpringBootApplication(exclude {DataSourceAutoConfiguration.class})但这样做要小心如果整个启动类排除了 DataSourceAutoConfiguration而你的服务又是纯数据访问服务可能连主库都连不上。所以这个排除动作不能照抄必须先确认你的若依版本到底有没有自定义数据源配置类。5.3 分页插件与达梦 SQL 方言冲突现象很典型同样的查询在 MySQL 数据源正常切到 DM 之后分页 SQL 变成奇怪的形式并在数据库中报错甚至直接把 PageHelper 的limit拼到 DM 不认识的语法上。原因是 PageHelper 有方言自动识别机制但不同版本对达梦的识别程度不一样。如果识别错了它会当成 MySQL 方言处理最终生成LIMIT ?, ?而 DM 在 Oracle 兼容模式下不认这种写法。处理方法在配置中显式指定helper-dialect: oracle让 PageHelper 用 Oracle 方言生成分页 SQL。如果还是不认单独给 DM 查询做分页时不要用 PageHelper 的startPage改用手动分页查询前根据自己的分页参数拼好ROWNUM或OFFSET FETCH子句。检查项目中是否有多个插件拦截器互相干扰。若依里如果同时存在 MyBatis 拦截器、数据权限拦截器和 PageHelper拦截顺序是会影响最终 SQL 的。顺序不对时PageHelper 生成的 count SQL 或分页 SQL 就可能被后续拦截器二次改坏。我曾经在 DM 数据源上遇到SQL 执行异常: 第1行 [LIMIT] 附近出现错误排查到最后发现竟然是数据权限拦截器在分页 SQL 基础上又拼了一段条件两者组合后导致 DM 的 SQL 解析器崩了。换成 MySQL 时因为兼容性好没暴露到了 DM 才原形毕露。5.4 事务、并发下的连接池耗尽问题多数据源下的连接池耗尽不一定是“连接数配大了就行”这么简单。我遇到过一种情况某个服务既连 MySQL 又连 DM在使用手动push切换数据源时因为最后一个poll没执行导致线程被污染。后续每次请求都绑定到 DM 数据源大量本应走 MySQL 的请求全部挤在 DM 的小连接池上而 MySQL 连接池几乎空转DM 连接池却被打满接口整体变慢甚至报wait connection timeout。这个问题的排查思路看监控哪个数据源的activeCount异常高。在日志中增加数据源切换的日志确认是否存在长时间未释放的push。全面搜索DynamicDataSourceContextHolder.push的代码检查poll是否都能在finally中执行。还有一种高手踩的坑DS(dm)的方法在往多线程里扔任务。比如方法 A 切换了 DM然后ExecutorService.submit开子线程去查数据。子线程用的是线程池里的其他线程这些线程没有绑定 DM 数据源标记于是子线程最终走了默认的 MySQL查询逻辑全错。这种问题比“串库”更隐蔽因为外层主线程的数据源看起来是切对了结果却不正确。解决办法是不要在多线程内部依赖DS最好在子线程任务里显式调用或者重新注入对应的 Service。5.5 高频问题速查表现象根因解决建议启动报找不到驱动类依赖没引入或未打包检查模块 pom 与最终 JAR 内容连接 DM 成功但 SQL 找不到表URL 未指定 schema在连接 URL 上加schema参数DS 注解不生效this 调用导致代理失效拆类或注入代理对象DS 下执行 MySQL Mapper 报错事务边界未拆分用 REQUIRES_NEW 拆事务分页 SQL 在 DM 上语法错误PageHelper 方言识别错误配置 helper-dialect 为 oracle 或手写分页DM 连接池被打满多线程切换标记污染push/poll 必须配对放 finally子线程查库怀疑数据源错动态数据源只绑定当前线程子线程内重新指定数据源这张表基本上覆盖了我这次改造中遇到的大部分问题。最后分享一点个人体会。多数据源引入以后最考验的不是第一次配置成功而是后续长期演进中能否保持清晰。我见过有的团队为了让一个查询方法少写几行代码把数据源切换藏在各种中间层里最后代码里到处是push排查问题翻半天。我更推荐的模式是连接 DM 的入口集中在一个独立的 Service 模块对外只暴露封装好的查询方法其他模块看到的是一个“数据服务”而不是“数据源”。这样即使以后 DM 老系统下线你只需要改这个独立模块删除数据源配置其他业务代码基本不用动。如果你正打算在若依微服务里接达梦先把依赖版本对齐再把事务边界想清楚最后才动手改业务代码。上面这些坑提前避过去整个落地过程会顺很多。
阅读完成 · 觉得有帮助?
咨询建站