多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

若依微服务集成MySQL+达梦双数据源:从依赖配置到踩坑实录

若依微服务集成MySQL+达梦双数据源:从依赖配置到踩坑实录 如果做过政企类项目大概率会遇到这种情况需求评审会上甲方的技术选型清单里写着“数据库达梦 DM”可你们团队基于若依微服务已经把核心业务跑了大半年所有库都在 MySQL 上。让甲方换库不现实把整条应用链路改成达梦风险又太大。我当时采用的方案是——保留 MySQL 作为主数据源新增一个达梦数据源负责对接监管报送库和第三方系统数据。这篇文章就把在若依微服务里把 MySQL DM 双数据源真正跑通的完整过程写出来从依赖引入、Nacos 配置、路由原理到代码落地的正确姿势以及达梦对接时最容易踩的几个坑给正在做信创适配或国产化改造的朋友一个可复用的参考。1. 场景梳理哪些项目需要 MySQL DM 双库并存1.1 国产化适配的典型业务诉求达梦数据库在国内政企、金融、能源行业的覆盖率逐年上升尤其是涉密等级较高或采购法明确的单位数据库选型会被直接限定为达梦 DM。但业务系统往往不是从零开始新建的基本都是已经跑了好几年的存量系统。存量系统的数据模型、SQL 方言、存储过程全按 MySQL 习惯写的硬迁到达梦不仅工作量大而且容易在细节上翻车。更常见的诉求其实是“新老并行”MySQL 继续承担在线交易类业务达梦作为监管报送、审计或第三方系统指定的数据源。两边数据通过定时任务或消息同步应用侧只需要做到“不同业务请求访问不同数据库”即可。这种需求不需要引入复杂的中间件一个轻量级多数据源方案就能解决。1.2 若依微服务默认的数据源架构RuoYi-Cloud 版本的微服务模块划分比较清晰gateway 网关、auth 认证、system 系统模块、generator 代码生成、job 定时任务、file 文件服务等。每个模块默认连接自己的业务库例如 system 服务对应主库ry-cloud相关配置集中在 Nacos 配置中心。这种架构下的多数据源配置不是在每个服务里重复配而是要看服务职责来定。一般只需要在真正访问达梦的业务服务里配置第二数据源其他服务保持原样。比如我做的项目里只有ruoyi-system服务需要读达梦库的监管数据那配置就集中在ruoyi-system.yml里其他服务完全不动避免所有服务都建立一堆没必要的连接池连接。顺带提醒一句很多朋友会把 RuoYi-Vue-Plus 和 RuoYi-Cloud 混淆。RuoYi-Vue-Plus 虽然叫“若依微服务plus”但它本质上是单体架构外加分布式组件不过它的多数据源能力是开箱自带的底层已经集成了dynamic-datasource-spring-boot-starter。而官方 RuoYi-Cloud 默认没有这个依赖需要手动引入。下文我会把两种情况的差异说到位避免配置时找不到对应位置。1.3 方案选型动态数据源还是分库中间件面对“多数据源”这三个字很多人的第一反应是上 ShardingSphere。但 ShardingSphere 解决的是分库分表、读写分离、数据脱敏这类复杂问题如果用它的核心能力搞“多数据源”反而重了。对于“一个业务方法需要访问 MySQL另一个业务方法需要访问达梦”这种场景最简单的方案是路由型多数据源。我当时对比过三种方案方案实现成本维护成本适合场景手写 AbstractRoutingDataSource中高极简场景但代码侵入强dynamic-datasource-spring-boot-starter低低若依这类已集成 MyBatis 的 Spring Boot 项目开箱即用ShardingSphere高高分库分表、读写分离、数据脱敏等复杂场景手写动态数据源不是不行但你需要自己处理连接释放、事务同步、数据源 key 的线程上下文很容易在并发测试时才暴露 bug。dynamic-datasource这个库把路由、切面、上下文清理都封装好了只需一个注解DS就能切换和 Spring 事务、MyBatis 配合也稳定是目前 Spring Boot 项目里最顺手的方案。2. 环境准备Maven 依赖与 DM JDBC 驱动安装2.1 达梦 JDBC 驱动的获取与本地安装达梦数据库的 JDBC 驱动并不在 Maven 中央仓库里这是第一个需要处理的问题。官方提供的驱动 JAR 包叫DmJdbcDriver18.jar支持 JDK 1.8 及以上版本。你需要从达梦官网或安装目录的drivers/jdbc目录下拿到这个 JAR。拿到 JAR 包后手动安装到本地 Maven 仓库命令如下mvn install:install-file -DfileDmJdbcDriver18.jar -DgroupIdcom.dameng -DartifactIdDmJdbcDriver18 -Dversion8.1.3.140 -Dpackagingjar如果是团队协作更合理的做法是搭建私有 Nexus 仓库把这个驱动上传到私服这样所有开发机都能正常拉取。否则每次换电脑都要重新install一次很容易出现“本地能编译、CI 上找不到驱动”的问题。2.2 在若依微服务中引入 dynamic-datasource 依赖官方 RuoYi-Cloud 默认没有集成多数据源需要在合适的公共模块的pom.xml中加入依赖。如果你用的是 Spring Boot 2.x引入dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version4.1.3/version /dependency如果你用的是 Spring Boot 3.x例如若依 Vue3 前后端分离版配套的新版本则需要使用dynamic-datasource-spring-boot3-starterdependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot3-starter/artifactId version4.3.0/version /dependency版本号建议锁定不要乱升级尤其是跨大版本时切面逻辑和注解包路径都可能调整。同时注意和项目里已有的 MyBatis、MyBatis-Plus 版本兼容实战中我遇到过因为依赖版本太新导致 Druid 循环依赖的问题锁定稳定版本后一切正常。2.3 RuoYi-Vue-Plus 和 RuoYi-Cloud 的差异提醒如果你用的是 RuoYi-Vue-Plus那只需要检查一下依赖是否已经存在一般是存在的直接跳过引入步骤。它的多数据源配置已经集成在系统模块里并预留了多个数据源的配置示例比如默认主库master额外例子可能是slave。而 RuoYi-Cloud 需要确认引入依赖的服务模块是否被 Maven 正确构建。因为若依把公共依赖放在ruoyi-common等模块里最稳妥的方式是把多数据源依赖放到ruoyi-common-mybatis或类似公共模块中所有业务服务都能继承避免在多个服务里重复加依赖。3. Nacos 配置中心双数据源 YAML 落地写法3.1 主从数据源的基础配置示例若依微服务的配置中心是 Nacos配置修改后需要发布到配置中心再重启服务生效。以ruoyi-system服务为例在 Nacos 中找到对应的ruoyi-system.yml添加如下配置spring: datasource: dynamic: primary: master strict: true datasource: master: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/ry-cloud?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_mysql_password dm: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: dm.jdbc.driver.DmDriver url: jdbc:dm://127.0.0.1:5236?compatibleModemysqlschemaSYSDBA username: SYSDBA password: your_dm_password druid: initial-size: 5 min-idle: 5 max-active: 20 validation-query: SELECT 1 test-while-idle: true time-between-eviction-runs-millis: 60000这段配置里有两个容易被忽略的关键点一是strict: true。它的作用是当代码里指定的数据源 key 不存在时直接报错而不是静默回落到主数据源。这里建议必须设置为true否则你代码里把数据源名拼错了请求悄悄落在 MySQL 上线上排查时够你找半天的。二是达梦的 URL。compatibleModemysql是达梦 JDBC 驱动支持的参数按 MySQL 兼容模式连接后SQL 方言上会更接近 MySQL 的习惯能大幅减少 Mapper 里的语法改动。如果你的达梦实例初始化时选的是 Oracle 兼容模式这个参数反而要谨慎设置具体差异我在后面踩坑章节详细展开。3.2 Druid 连接池与监控参数的兼容设置若依默认使用 Druid 连接池多数据源配置下的 Druid 参数可以统一维护在spring.datasource.dynamic.druid下。这里有几条实用建议validation-query统一写成SELECT 1MySQL 和达梦都支持如果达梦库是低频查询建议给dm数据源单独设置更小的连接池参数比如max-active: 10避免为低频场景预留太多连接资源test-while-idle打开避免长时间空闲的连接被数据库端主动断开后应用侧不知情拿到一个死连接报错。还有一个细节是 Druid 监控页。若依自带 Druid 监控页面/druid路径但动态数据源底层会自动创建多个 DruidDataSource这时候原来的DruidConfig可能会和这边的配置冲突。如果发现监控页面数据不正常优先检查项目里是不是存在旧的DruidConfig手动创建数据源的逻辑有冲突的话建议改造为统一走spring.datasource.dynamic.druid配置。3.3 刷新配置后如何验证生效Nacos 配置发布后重启ruoyi-system服务观察服务日志。若依的日志会打印 Druid 连接池启动信息确认两个数据源都初始化成功。然后写一个临时接口做冒烟验证不要直接查业务表先用最简单的 SQL 试通路RestController public class DataSourceCheckController { Autowired private JdbcTemplate jdbcTemplate; GetMapping(/check/dm) public String checkDm() { // 如果当前线程数据源指向 dm这里会从达梦连接池拿连接 return String.valueOf(jdbcTemplate.queryForObject(SELECT 1, Integer.class)); } }在调用该接口前先想办法让DS(dm)生效更简单的方式是直接在 Mapper 接口上打注解加一个对应的接口方法执行SELECT 1。能返回 1说明路由正常、连接正常接下来再做业务表验证。4. 核心原理DS 注解与数据源路由切换的底层逻辑4.1 AbstractRoutingDataSource 的动态路由机制很多读者可能好奇一个注解凭什么能切换数据源原理并不复杂。Spring 本身就提供了AbstractRoutingDataSource这个抽象类它会在地获取连接时调用determineCurrentLookupKey()方法根据返回的 key 从目标数据源映射表里选一个真正的数据源。dynamic-datasource就是在这一基础上做了包装DS注解通过 AOP 切面在方法执行前把数据源 key 放到DynamicDataSourceContextHolder的 ThreadLocal 里方法执行完再把 key 移除。这样每次数据库操作都会按当前线程保存的 key 去路由连接。DS(dm) public ListDmDeviceVO listDmDevices() { return dmDeviceMapper.selectList(); }这段代码执行时AOP 会把dm放入上下文MyBatis 底层拿连接时就会从达梦连接池获取连接。4.2 DS 和 Transactional 的边界问题这是整个多数据源配置里最容易踩坑的地方。Spring 的Transactional一开启事务管理器就会从当前数据源获取一个连接并绑定到当前线程整个事务期间都用这个连接。如果同一个方法里既加了Transactional又通过DS切换数据源后加的DS往往不会生效因为事务已经和某个数据源绑定死了。最典型的问题是方法先开启 MySQL 事务然后调用一个标注了DS(dm)的 Mapper 方法期望它写入达梦。结果实际操作还是落在 MySQL 上或者直接报事务连接异常数据写错库的后果比报错更严重。我的建议是严格遵守一个原则一个业务方法只碰一种数据源跨库操作拆成多个方法。如果确实需要在一个业务操作里同时查 MySQL 和达梦的数据可以在 Service 层分别调用两个方法最后在 Controller 或外层 Service 做结果组装。例如Service public class DeviceBizService { Resource private DeviceService deviceService; Resource private DmRegulatorService dmRegulatorService; public DeviceDetailVO detail(Long id) { DeviceDetailVO detail new DeviceDetailVO(); // 从 MySQL 查设备主信息 detail.setDevice(deviceService.getById(id)); // 从达梦查监管信息内部通过 DS(dm) 切换 detail.setRegulator(dmRegulatorService.getByDeviceId(id)); return detail; } }注意这里dmRegulatorService.getByDeviceId(id)如果打的是DS(dm)那么该方法的内部实现里不能再添加Transactional否则切换可能失效。查询场景一般不需要事务所以问题不大。4.3 同类内部调用导致 DS 不生效另一个隐蔽的坑是 Spring AOP 的内部调用问题。如果一个类内部用this.xxx()调用自己的另一个方法即使被调用的方法上打了DS(dm)切面也不会拦截因为从this调用时没有经过 Spring 代理对象。Service public class DeviceService { public void switchCall() { // 这里的 this.callDm() 不会触发 DS this.callDm(); } DS(dm) public void callDm() { // 实际拿到的还是主数据源连接 } }解决办法有三种一是把切换数据源的方法放到另一个独立 Bean 里二是注入自身代理对象三是在类级别加DS(dm)整个类默认都走达梦。对于我经历的项目第三种方式最简单实用尤其是某个 Mapper 或 Service 本身就是专门服务达梦的直接在类上标注比每个方法都加注解更不容易遗漏。5. 完整代码示例从 MySQL 读订单、从达梦读监管报表5.1 Service 层与 Mapper 层的注解使用以实际项目为例我需要把订单系统的设备信息从 MySQL 读出来同时从达梦库读取该设备的监管报送记录。两个库的表结构和字段完全独立业务上只需要按某个设备编号做关联。首先是连接达梦的 MapperDS(dm) public interface DmRegulatorMapper { ListDmRegulatorVO selectByDeviceId(Param(deviceId) Long deviceId); }如果这类 Mapper 只服务达梦数据源直接在接口上加DS(dm)就够了所有方法统一走达梦不会漏加。然后是 MySQL 侧的 ServiceService public class DeviceService { Resource private DeviceMapper deviceMapper; Resource private DmRegulatorMapper dmRegulatorMapper; public DeviceDetailVO detail(Long deviceId) { DeviceDetailVO detail new DeviceDetailVO(); // 默认走主数据源 MySQL detail.setDevice(deviceMapper.selectById(deviceId)); // 直接调用达梦 Mapper 方法注解切面会自动切换 detail.setRegulator(dmRegulatorMapper.selectByDeviceId(deviceId)); return detail; } }这里有个细节DmRegulatorMapper是另一个 Bean所以调用它时 Spring AOP 会正常拦截DS切面生效不会有内部调用问题。这个写法在多个数据源场景下非常干净值得推荐。5.2 Mapper XML 中 SQL 方言兼容的处理方法MySQL 和达梦虽然都支持标准 SQL但细节语法有差异。如果只是查询和简单关联大部分 SQL 可以通用一旦涉及到分页、函数、关键字就要格外小心。以分页为例达梦在兼容 MySQL 模式下支持LIMITSELECT * FROM dm_regulator WHERE device_id #{deviceId} LIMIT 0, 10如果达梦实例初始化时选择了 Oracle 兼容模式标准的LIMIT语法可能就不被支持需要改成ROWNUM方式。所以我的建议是达梦实例初始化时尽量选 MySQL 兼容模式这样代码里已有的大量 MySQL 方言 SQL 可以直接跑改造量最小。另一个需要注意的细节是表名和字段名的双引号问题。MySQL 习惯用反引号包裹表名达梦默认使用双引号。如果 Mapper XML 里写了 MySQL 风格的反引号在达梦上很可能直接报语法错误。统一移除反引号或者按目标数据源分开维护 XML 文件。5.3 只有查询需求时的只读数据源优化如果达梦数据源只做查询不做写入可以进一步优化连接池配置dm: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: dm.jdbc.driver.DmDriver url: jdbc:dm://127.0.0.1:5236?compatibleModemysqlschemaSYSDBA username: SYSDBA password: your_dm_password read-only: true max-active: 10 initial-size: 2read-only: true后连接池创建的所有连接都标记为只读数据库端可以走只读优化路径。同时缩小max-active避免为低频查询保留过多主动连接。这个细节在政企项目的服务器资源普遍紧张的背景下很实用。6. 踩坑实录达梦对接过程中最常见的 5 个问题6.1 关键字冲突导致建表和查询失败达梦保留了一大批关键字TYPE、SIZE、LEVEL、USER这些在 MySQL 里可以当字段名的词在达梦里直接裸写会报“无法解析”错误。当时我们有一张设备表字段叫TYPE在 MySQL 里跑得好好的切到达梦后 SQL 直接失败。解决办法是把关键字用双引号包裹例如SELECT TYPE FROM device_info。如果不想每次写 SQL 都带双引号更彻底的方案是迁移建表时把字段改名为device_type或type_code一劳永逸。我的建议是优先改名字因为双引号包裹虽然能解决问题但在和其他系统对接、报表工具取数时容易再次踩坑。6.2 连接时报 SSL 类错误如果你把 MySQL 的连接参数习惯性抄到达梦 URL 里很容易出问题。常见的错误现象是SQLNonTransientConnectionException或“通信链路失败”。一个非常典型的案例是 URL 里带了 MySQL 专属的useSSLfalseserverTimezoneAsia/Shanghai参数。达梦 JDBC 驱动不认识useSSL和serverTimezone连接握手阶段就可能异常。解决起来很直接达梦的 URL 参数单独按达梦文档来写不要复用 MySQL 的参数。另一个可能原因是达梦服务端口不通需要确认5236端口在防火墙里放行。6.3 PageHelper 分页方言冲突若依官方 RuoYi-Cloud 项目使用 PageHelper 做分页而 PageHelper 的方言判断通常依赖数据源 URL。当请求切换到达梦数据源时分页插件可能仍按 MySQL 方言生成分页 SQL导致语法错误。如果你用的是 RuoYi-Vue-Plus它内置了 MyBatis-Plus 的分页插件这个插件能根据当前数据源自动识别方言体验更顺滑。而 RuoYi-Cloud 手动引入的 PageHelper 则需要特别注意一个可用的办法是设置全局方言为达梦pagehelper: helper-dialect: dm reasonable: true support-methods-arguments: true但要意识到设置成达梦方言后MySQL 数据源的分页 SQL 也可能被改写成达梦风格两边可能互斥。我在项目里最终的方案是达梦查询不走 PageHelper而是在 Mapper XML 里手写LIMIT #{offset}, #{size}配合达梦 MySQL 兼容模式使用MySQL 侧继续用 PageHelper两边井水不犯河水。6.4 驱动版本不一致导致启动失败团队协作时最常见的是“我本地能跑你本地报 ClassNotFound”。原因就是达梦驱动 JAR 没有统一管理。Maven 仓库没有官方坐标很多人直接拷 JAR 到某个模块的lib目录这个模块构建时能通过但其他模块引不到。正确做法是至少把 JAR 打到本地 Maven 仓库或私服然后在pom.xml里声明依赖这样所有模块统一走 Maven 构建。另外要确认驱动版本和 JDK 版本匹配DmJdbcDriver18要求 JDK 8 及以上如果你的服务还跑在 JDK 7 上就得换旧版驱动但那种场景现在已经很少见了。6.5 大小写策略导致表名找不到达梦实例初始化时可以选择“大小写敏感”或“大小写不敏感”。如果选了大小写敏感不带双引号的标识符会被自动转成大写。而 MySQL 中建表通常是小写表名迁移到达梦后代码里写dm_device实际查询的是DM_DEVICE自然报“无效的表名或视图”。这个坑的修复成本最高因为一旦数据库实例初始化时定下大小写策略后面很难全局调整。我的建议是初始化达梦实例时直接选择“大小写不敏感”并把表名统一用大写或小写一种风格。如果已经是大小写敏感的实例只能在建表时统一用双引号包裹并把 SQL 里所有表名、字段名都加上双引号工作量确实偏大。7. 多数据源下的架构边界与性能建议7.1 连接池规模与初始化策略引入双数据源后每个数据源都是独立的 Druid 连接池连接数、内存、监控维度都会翻倍。建议按服务实际压力区分主数据源 MySQL保持原有参数。若依默认是initial-size: 5、max-active: 20按常规业务并发可以。达梦数据源如果是低频查询initial-size: 2、max-active: 10已经足够。这里要提醒一个问题Druid 初始化连接池时如果连不上数据库默认会抛异常导致服务启动失败。如果达梦只是辅助库且经常在维护窗口期停机建议把达梦数据源的初始化参数调小并做好服务启动的容错。最稳妥的做法是在配置中心维护一个开关达梦不可用时通过配置关闭该数据源而不是让整个微服务直接挂掉。7.2 跨库事务的取舍两个数据源分属不同数据库严格意义上这是一个分布式事务问题。第一原则是不要在一个Transactional方法里同时操作 MySQL 和达梦前面已经说过事务绑定一个数据源后切换数据源会失效。如果你确实需要两个库的写入保持最终一致性最简单的方案是使用若依自带的定时任务模块实现“先写主库、再异步同步到达梦”的方式配合失败重试和日志记录。若对一致性要求更高才考虑引入 Seata AT 模式但会增加全局事务管理器、事务协调等组件部署和运维复杂度明显上升。对于大多数政企项目的监管报送场景T1 或准实时的最终一致性完全够用没必要为了做大做强引入分布式事务。7.3 数据同步时的字段类型映射参考如果 MySQL 业务库的数据需要定期同步到达梦字段类型映射需要提前规划好。下面的表是我在一个实际迁移项目里整理出来的对应关系MySQL达梦 DM8备注tinyint(1)SMALLINT若映射为 BIT 或 BOOLEAN 容易出现兼容问题int / int unsignedINT / INT一般直接兼容bigintBIGINT直接兼容varchar(n)VARCHAR(n)注意达梦 VARCHAR 最大长度受页大小影响textTEXT / CLOB超过阈值用 CLOB 更稳妥datetimeTIMESTAMP / DATETIME兼容模式不同表现有差异decimal(18,2)DECIMAL(18,2)直接兼容还有一个容易忽略的坑MySQL 的自增主键auto_increment在达梦里需要改成IDENTITY或使用序列直接复制建表语句会失败。如果只是数据同步查询可以忽略主键自增策略如果要迁达梦库的写入逻辑就必须重新设计主键生成方案。最后一点经验多数据源配置本身不难难的是把边界设计清楚。我在实际项目中体会最深的一点MySQL 和达梦的职责划分要在一开始就明确。比如 MySQL 管在线交易和设备主数据达梦只做监管报送和归档查询然后严格约定每个 Service 方法只碰一种数据源跨库场景统一走结果组装或异步同步。这样即使后续要做更深入的国产化迁移代码层面也不至于伤筋动骨。另外给自己留一个排查线索数据源命名尽量带上业务含义比如master和dm_report日志里打印数据源标识。线上出现问题第一件事就是判断请求到底走了哪个库能少走很多弯路。
返回列表