ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

若依微服务接达梦DM双数据源实战:动态切换与踩坑排查

若依微服务接达梦DM双数据源实战:动态切换与踩坑排查 前阵子我在做一个老系统的国产化适配业务底座是若依微服务RuoYi-Cloud历史数据全在 MySQL 里。按客户要求新增模块的数据必须直接落到 DM 达梦数据库而且两个库之间还有联动校验。最初我想的很简单不就多配一个数据源吗结果从驱动的 jar 处理、数据源配置结构到事务切面的执行顺序、分页 SQL 的数据库类型识别几乎每一步都踩了一遍。这篇文章就把我在若依微服务里配置 MySQL DM 多数据源的完整过程和最终方案分享出来适合正在做国产化改造、又不想被中间件绑死的 Java 项目团队参考。读完你会得到三样东西一套可直接抄作业的动态数据源配置几个和若依微服务结合的靠谱切入点以及我实测下来最常踩的坑和排查思路。1. 用上双数据源的真实场景为什么不能简单把表搬过去1.1 跨库查询与逐步迁移的现实需求先说说这次为什么要搞双数据源。接手的是一个跑了几年的业务系统核心数据都存在 MySQL库表结构已经比较稳定。但是新上线的几个模块按照验收要求数据必须写进国产数据库 DM 里。问题在于这些模块并不是完全独立的它们要和老模块的数据做对账、做校验比如新模块登记一条记录需要反查老模块里的历史订单信息。数据量不小不可能一夜之间把所有 MySQL 表全量迁到 DM。现实的做法是一段一段切老的 MySQL 继续作为主库处理存量逻辑新的 DM 库承载新增业务等两边数据验证稳定了再逐步把更多表迁过去。这种“渐进式国产化替换”的模式在现在的项目里非常常见而落到代码层面就要求同一个微服务进程里既要有 MySQL 的 mapper也要有 DM 的 mapper并且能在运行时按需切换。1.2 为什么不是中间件而是应用层多数据源有人可能会问跨库访问为什么不上数据库中间件或者用数据同步工具先把 MySQL 的数据同步到 DM我之前也考虑过但实际排除了。数据同步工具适合离线场景实时性不够而且两边字段类型不完全一致同步规则很麻烦。数据库中间件则意味着整个数据访问链路要改架构若依微服务本身已经是一套比较完整的体系为了一个改造项目去增加新的基础组件风险和工作量都不小。应用层做多数据源反而是收益最快的方式。核心原理就是 Spring 的 AbstractRoutingDataSource维护一个数据源映射表每次获取数据库连接之前通过 ThreadLocal 里保存的数据源标识动态决定连 MySQL 还是连 DM。这样业务代码只需要用注解标记其他逻辑照旧。若依的单体版本本来就内置了类似的 DataSource 切换机制我这次做的事情相当于在微服务版里把这套能力补齐。2. 若依微服务默认数据源机制与改造切入点2.1 默认配置长什么样先看若依微服务默认的数据源配置。每个业务服务的配置文件里通常是这样一段spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver druid: url: jdbc:mysql://localhost:3306/ry_cloud?useUnicodetruecharacterEncodingutf8zeroDateTimeBehaviorconvertToNull username: root password: 123456若依微服务的理念是“一个服务连一个库”服务之间通过 Feign 调用或者分布式事务组件协作。它不像单体版那样把 DataSource 注解作为开箱即用的功能暴露出来所以第一次接手的人很容易产生两个误解要么以为微服务版不支持多数据源要么以为直接复制单体版的配置就能用。这两个误解我都在项目里见过。2.2 改造的三种路线对比我当时对着若依微服务的数据源体系权衡了三条路线。第一条是完全自研动态数据源。自己实现 AbstractRoutingDataSource配合 AOP 切面拦截自定义注解。代码量不大但后续要自己维护切面顺序、事务绑定、多环境配置、连接池监控这些细节加起来工作量并不小。第二条是使用开源动态数据源组件 dynamic-datasource-spring-boot-starter。它把多数据源的配置、切换、分组、事务绑定都封装好了业务代码只需要一个 DS 注解配置文件和 Spring Boot 原生风格一致对 MyBatis-Plus 兼容性好。社区活跃版本迭代勤快。第三条是彻底放弃切换机制直接为每个数据源建立独立的 MyBatis 配置路径。比如一套 mapper 包专门扫 MySQL另一套扫 DM互不干扰。这种方式直白但跨库方法论里就只能同时注入两套 mapper代码侵入大而且 SQL 边界容易模糊。最终我选了第二条。主要原因是本项目的数据访问层已经深度依赖 MyBatis-Plusdynamic-datasource 对 MyBatis-Plus 的适配做过专门优化和 Druid 连接池也能无缝组合。下面正文里的所有步骤都基于这个方案。3. 完整配置步骤接入 DM 数据源落地全过程3.1 依赖安装与驱动引入DM 数据库的 JDBC 驱动类名通常是 dm.jdbc.driver.DmDriver官方提供的驱动包一般是 DmJdbcDriver18.jar。这个驱动包并不在 Maven 中央仓库上收录我拿到的是项目方直接给的 jar所以第一步是把驱动安装到本地 Maven 仓库或者放到私有 Nexus 上。我用的命令是mvn install:install-file -DfileDmJdbcDriver18.jar -DgroupIdcom.dameng \ -DartifactIdDmJdbcDriver18 -Dversion8.1.2.192 -Dpackagingjar安装成功后在业务服务模块的 pom.xml 里引入驱动同时引入动态数据源组件dependency groupIdcom.dameng/groupId artifactIdDmJdbcDriver18/artifactId version8.1.2.192/version /dependency dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.6.1/version /dependency版本号以你实际拿到的驱动为准我这只是示例。这一步有个很容易忽略的点若依微服务默认配置里写的是spring.datasource.druid.url这种嵌套结构而动态数据源组件读取的是spring.datasource.dynamic.datasource下面的内容。如果你只是把组件引入进来不改配置结构组件会直接忽略你原本写好的数据源服务照样启动但所有连接都会变成默认行为。这是我在联调早期遇到的最迷惑的问题之一。3.2 数据源配置与动态数据源开关在 application.yml 中我把数据源配置改成了这样spring: datasource: dynamic: primary: mysql strict: false datasource: mysql: url: jdbc:mysql://localhost:3306/ry_cloud?useUnicodetruecharacterEncodingutf8zeroDateTimeBehaviorconvertToNull username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 min-idle: 5 max-active: 20 dm: url: jdbc:dm://localhost:5236/DM_TEST username: SYSDBA password: SYSDBA driver-class-name: dm.jdbc.driver.DmDriver druid: initial-size: 3 min-idle: 3 max-active: 10几个关键配置我解释一下。primary指定默认数据源是 mysql这样所有没有标注 DS 的 mapper 仍然全走 MySQL老代码几乎零修改。strict我设置成 false含义是当代码里写了一个不存在的数据源名称时回退到 primary服务不会因为名称拼写错误就直接启动失败。这在多人协作开发时很友好。另外要重点处理 Druid 自动配置的冲突。由于若依微服务本身已经引入 Druid如果不做排除DruidDataSourceAutoConfigure 会自动注册一个默认数据源动态数据源组件可能根本不会生效。我在 yml 里加了spring: autoconfigure: exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure如果你的项目里实际没有 Druid 的自动配置类这一步可以忽略但我强烈建议先确认。因为我踩到过的现象是服务能正常启动yml 里数据源也写得没错但运行期的 SQL 全部走 MySQL怎么加 DS 都没反应排查了半天才定位到是自动配置冲突。3.3 业务代码中的切换与验证配置没问题后业务代码里的切换就很简单了。在 Service 方法上加注解DS(dm) public ListDmOrderVO listFromDm(DmOrderQuery query) { return dmOrderMapper.selectList(query); }如果某个 Mapper 的所有操作都只针对 DM 库可以直接在 Mapper 层标注这样后续任何 Service 注入这个 Mapper走的都是 DMDS(dm) public interface DmOrderMapper extends BaseMapperDmOrder { }我个人的习惯是优先在 Service 层控制因为 Mapper 层一旦复用或者被多个业务模块共用数据源会变得不好追踪。只有那些完全独立、不会被复用的 DM 专用查询才放在 Mapper 层标注。验证阶段不要只看服务启动成功就宣布完事。我在项目里做了双向验证先调用一个老接口确认 MySQL 侧有会话产生再调用带 DS(dm) 的新接口然后到 DM 数据库的管理视图里观察是否有新连接进来。如果 DM 侧始终没有连接那就是切面没生效优先检查第 3.2 步的排除配置和事务拦截器。4. 事务、连接池与分页三个最容易翻车的细节4.1 数据源切换与事务的先后顺序动态数据源组件的基本原理不复杂AOP 拦截带 DS 注解的方法把数据源标识写进 ThreadLocal然后 AbstractRoutingDataSource 在每次获取连接时读取标识决定返回哪个数据源。看起来顺理成章但一旦方法上同时出现 Transactional事情就变了。Spring 的事务管理本身也是靠 AOP 拦截器实现的。如果事务拦截器先于数据源切换切面生效那么连接早就在方法开始时从默认数据源拿好了后面 ThreadLocal 里的标识再变也已经来不及。现象就是DM 数据源配置完全正确但带 DS 的方法执行后日志里显示的 SQL 还是跑在 MySQL 上。解决办法大体有两类。一类是调整切面执行顺序让数据源切换先于事务开启。另一类是避免在同一个事务方法内切换数据源把跨库操作拆成两段——先在一个非事务方法里拿 DM 的数据再把结果传给事务方法去处理 MySQL。dynamic-datasource 也提供了 DSTransactional 注解做事务绑定但我的实际建议是不要轻易在同一个事务里同时操作 MySQL 和 DM。一旦后续要回滚分布式事务的复杂度和排查成本都会翻倍改造项目承受不起。我在这次项目里定的规矩就是一个事务方法只操作一个数据源跨库场景要么拆成两步要么通过消息异步同步这不光规避了切面顺序问题也让整个项目的数据库访问边界变得清晰。4.2 连接池监控与参数隔离MySQL 和 DM 的连接池参数绝对不能照搬。DM 服务端的连接数上限通常比 MySQL 保守如果直接把 MySQL 的 max-active20 复制过去压测时 DM 可能最先被击穿新连接全部排队接口超时一个接一个来。我给两个库分别设置了不同的连接池参数还把 Druid 监控接上了。dynamic-datasource 对 Druid 监控的支持比较完整只要依赖中有 Druid监控页面就能看到 mysql 和 dm 两组数据源各自的活跃连接数、等待次数、慢查询数。上线前用测试工具压了一轮重点观察 DM 连接池里等待连接的时间曲线然后再微调 max-active、min-idle 和 max-wait。这个步骤不能省生产环境里很多偶发性的数据库连接异常都是因为连接池参数没按库单独调。另外一个关于 DM 连接串的注意事项连接串里最好显式指定 schema比如jdbc:dm://localhost:5236/DM_TEST。如果不写 schemaDM 会根据登录用户的默认 schema 去解析表名时常出现“表或视图不存在”的报错。这个问题在现场很有迷惑性因为看起来明明是在访问同一个库却访问不到目标表。4.3 DM 方言与分页插件适配若依微服务的数据访问层默认用 MyBatis-Plus分页靠 PaginationInnerInterceptor。这个插件需要明确知道数据库类型否则分页 SQL 会生成错误。在 DM 场景下插件从连接元数据自动识别数据库类型并不总是可靠有些 DM 版本的驱动元数据识别不到分页会退化甚至直接报错。最稳妥的做法是在分页拦截器初始化时显式指定PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.DM); pagination.setMaxLimit(500L);另外DM 数据库存在兼容模式的概念。如果你的 DM 实例是按 Oracle 模式初始化的SQL 里就不能沿用 MySQL 的反引号写法如果兼容 MySQL函数和语法会接近一些但也不完全一致。我在实践里遇到过的 MySQL 函数比如 DATE_FORMAT、GROUP_CONCAT在 DM 里往往没有同样的行为必须改写。所以我的建议是永远不要追求“一份 SQL 两个库都能跑”。每个 Mapper 的 SQL 严格按照它绑定的数据库类型去写尤其在双数据源模式下写 SQL 前先确认这个 Mapper 到底最后落在哪个库。5. 实测踩坑清单与排查链路5.1 报错信息与根因对照下面把我在这个项目里实际遇到的高频问题整理一下方便你在实施时快速定位现象可能的报错或行为根因解决办法服务启动失败提示找不到数据源Cannot find datasource配置结构还是 Spring Boot 原生格式动态数据源没有读到改成spring.datasource.dynamic.datasource的结构所有 SQL 仍然走 MySQLDS 无效监控里 DM 库毫无请求Druid 自动配置覆盖了动态数据源排除 DruidDataSourceAutoConfigureDM 查询报“表或视图不存在”DM 侧返回类似 ORA-00942 的提示连接串没有指定 schema或表名大小写不一致连接串显式指定 schema表名字段名按 DM 实际规则保留分页查询结果错乱返回条数不对count 语句报错分页插件没有识别到 DM 类型显式设置 PaginationInnerInterceptor(DbType.DM)加了事务后切换失效异常时部分数据已提交事务拦截器先于数据源切面执行拆分事务方法或使用 DSTransactional避免跨库事务5.2 一个完整的排查案例DS 完全没生效这个案例是我这次改造中最典型的一次排查过程很有分享价值。现象是某些上报文件只在某个业务服务里写死一个数据源其他服务都正常。后来发现是启动类上的自动配置问题。再往后在一个原本正常的服务里新增 DM 数据源无论怎么加 DSDM 侧就是没有连接进来。排查的时候我先停掉了方法上的 Transactional再调用一次发现 DS 生效了。到这里基本确定是事务切面和数据源切换切面的顺序问题。但紧接着我检查了调用链发现这个 Service 方法其实是被 Feign 入口调用的外层的方法打了一个事务模板注解内部查询被包在这个已有事务里。内层方法上的 DS 即使拦截到了ThreadLocal 里的数据源标识也已经无法改变已经绑定的事务连接。解决办法不是去调切面顺序而是把外层 Feign 入口方法改成纯转发入口方法不碰数据库只接收参数再调用一个新的无事务内部 Service 方法由内部方法完成带 DS 的查询。这个改动不复杂但效果立竿见影也避免了去动全局事务配置带来的未知影响。这个案例也让我总结出了一条判断经验多数据源出问题时不要一上来就怀疑组件坏了优先按三件事排查——确认事务上下文确认配置结构确认切面顺序。大多数所谓“切面失效”最后都能在这三件事里找到答案。6. 如果后续要继续改造版本升级与渐进迁移的一些建议6.1 组件版本升级时的兼容性检查dynamic-datasource 的版本不是越新越好。我在升级过程中观察到新版本对 Spring Boot 3.x 的支持更完善但若依微服务如果还停留在 Spring Boot 2.x就不能盲目升级主版本否则数据源自动配置行为会发生变化。升级之后最直接的检查点有两个。第一个是启动类上的自动配置排除是否仍然有效第二个是所有带 DS 注解的服务启动后去两个库的管理视图里分别确认连接数。我一般会先在一个测试环境服务上升级跑完所有的多数据源用例再推广到其他服务。6.2 从双数据源到单一 DM 的平滑过渡改造项目的最终目标往往是全部迁到 DM。在双数据源运行一段时间后可以把读写流量逐步从 MySQL 切换到 DM先用只读方式验证查询结果一致性再切换到写入最后把 MySQL 侧只保留只读备份。这个过程中有一个容易忽略的细节表结构里的自增主键、时间字段默认值、字符串类型长度限制在两套库里可能都有差异。切换前建议先做一轮字段映射检查而不是简单地把 SQL 搬过去。双数据源模式的真正价值就是给你争取了这段过渡时间让替换动作不必一步到位。我在这个项目里最大的体会是多数据源的技术实现并不复杂真正的复杂度都藏在边界条件里比如事务、方言、连接池、切面顺序。只要一开始就把这些边界想清楚配置双数据源不仅能解决国产化替换问题也会让后续的数据迁移灵活很多。希望这篇文章能让你少走我之前走过的弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进