RuoYi-Vue-Plus多数据源实战:配置、切换与事务避坑指南
做 Java 后台管理系统开发的朋友对 RuoYi-Vue-Plus 应该都不陌生RBAC 权限、代码生成、定时任务这些开箱即用的能力确实省了不少事。但业务一旦跑起来很多人就会遇到一个绕不开的需求多数据源。最简单的场景是读写分离主库负责写业务数据从库跑报表统计再复杂一点的是同一个系统同时对接多个业务库比如把订单库、用户库、采购库拆到不同的 MySQL 实例上。RuoYi-Vue-Plus 底子是支持多数据源的框架已经集成了动态数据源组件但配置入口、注解用法、事务之间的坑都不少网上很多资料又只讲一半。这篇文章我从原理到实操把全过程完整拆一遍适合在 RuoYi-Vue-Plus 上做二次开发、又需要连多个数据库的同学也适合那些已经配了多数据源但切换不生效、正在排查问题的人。1. RuoYi-Vue-Plus 多数据源底层到底靠什么撑起来的1.1 dynamic-datasource 组件是整套机制的基石RuoYi-Vue-Plus 本身没有自己造一套多数据源轮子而是直接集成了苞米豆团队出品的dynamic-datasource-spring-boot-starter。这个组件在 Spring Boot 生态里几乎是多数据源的标配MyBatis-Plus 的作者团队维护的跟 MyBatis-Plus 的兼容性很自然。它做了几件关键的事自动装配多个真实数据源、通过注解或手动方式切换路由、支持 Druid/HikariCP 等连接池混用、给 MyBatis-Plus 提供无缝适配。打个比方你可以把每个真实数据源想成墙上的独立插座Druid 或者 HikariCP 是插头而动态数据源组件是一个智能排插。这个排插统一对外暴露一个DataSource接口所有代码在获取数据库连接时先问排插“这次要走哪个插座”排插再决定把请求分给哪个底层数据源。这样做的好处是业务代码里基本感知不到多数据源的存在注入DataSource或者SqlSessionFactory的时候拿到的都是这个路由层切换只发生在一个很薄的环节上。1.2 ThreadLocal 栈式路由与 AOP 拦截的配合用一句话说穿底层原理动态数据源在每次获取连接时根据一个当前线程上下文中保存的 key 来决定路由到哪个真实数据源。这个 key 保存在DynamicDataSourceContextHolder里面它的内部是一个ThreadLocalDequeString你没看错是个双端队列栈结构而不是普通变量。为什么要用栈因为业务场景里会发生数据源切换嵌套。比如外层 Service 方法走 master方法内部又要调另一个 Service 的查询走 slave查完还得回到 master。用栈结构就能保证切换信息的先进后出push进一个 key用完后poll弹出来外层上下文不会丢失。如果是普通变量内层切换一覆盖外层再想恢复就找不回来了。这个细节是很多自研多数据源方案翻车的重灾区。AOP 拦截这部分也不复杂。DS或者 RuoYi-Vue-Plus 封装的DataSource注解会被DynamicDataSourceAnnotationAdvisor拦截。拦截器在进入目标方法之前解析注解值并调用DynamicDataSourceContextHolder.push(key)方法执行完之后在 finally 块里poll()弹栈。Spring 的AbstractRoutingDataSource在getConnection()时通过determineCurrentLookupKey()拿到当前线程栈顶的 key从而选中目标数据源。还有一点值得提strict参数控制找不到 key 时的行为。strict: false时找不到就直接回退到primary主数据源strict: true时报错。开发阶段很多人图省事用 false结果把“切换不生效”这种问题掩盖了因为 key 写错也会静默走主库定位起来很费劲。1.3 框架自带的 DataSource 与原生 DS 之间的关系RuoYi-Vue-Plus 在ruoyi-common-datasource模块里封装了一个DataSource注解底层其实就是DS的二次封装。它内部用DS作为元注解标记并提供DataSourceType常量类里面定义了MASTER master和SLAVE slave两个数据源 key。这样设计主要是为了框架内部统一风格不让字符串散落在各处同时也方便后续扩展其他数据源名称。实际使用中两者是可以混用的。如果项目跟着 RuoYi-Vue-Plus 的规范走就优先用com.ruoyi.common.datasource.annotation.DataSource如果团队更习惯 MyBatis-Plus 生态原生的写法直接用com.baomidou.dynamic.datasource.annotation.DS也完全没问题。要注意的是它们的方法优先级规则一致方法上的注解优先级高于类上的注解。也就是说类上标了 slave某个方法上标了 master那这个方法实际走 master。2. 配置一个能跑起来的双数据源2.1 版本与依赖检查别在第一步埋雷动手配置前先确认项目里 dynamic-datasource 的版本。RuoYi-Vue-Plus 新版本已经升级到 Spring Boot 3.x JDK 17这种情况下 dynamic-datasource 需要 3.5.x 以上才兼容 Spring Boot 3 的自动装配机制。如果你还在维护老项目基于 Spring Boot 2.x 的 RuoYi-Vue-Plus用的 dynamic-datasource 版本又是 3.3.x 那种也可以继续跑但千万别把一个 3.5.x 的依赖硬塞进 Spring Boot 2 项目里分分钟给你整出循环依赖或者 Bean 创建异常。最简单的方法是直接看 RuoYi-Vue-Plus 父 pom 里dynamic-datasource-spring-boot-starter的版本号不要凭感觉填。RuoYi-Vue-Plus 理论上已经帮你协调好了只要不手动改版本一般不会出问题。如果你在自己维护的一版工程里看不到这个依赖可以检查ruoyi-common-datasource模块的 pom它通常间接引用了动态数据源组件。2.2 环境准备主从库、账号权限、驱动配置多数据源之前先把数据库环境准备好。最典型的是一主一从主库写业务数据从库给报表、统计、历史查询用。从库账号建议单独创建尽量只给 SELECT 权限防止程序在从库里跑出写操作。别小看这一步从库一旦被写入脏数据跟主库的数据对账会让你怀疑人生。驱动方面如果两个库都是 MySQL直接确认driver-class-name是com.mysql.cj.jdbc.Driver如果你要接 PostgreSQL、Oracle、SQL Server那要看具体驱动类名并且确保 jar 包已经引入。很多报错“无法加载驱动类”都不是配置问题而是 pom 里根本没加对应数据库的驱动依赖。RuoYi-Vue-Plus 默认工程里一般只有 MySQL 驱动接其他库得自己补。2.3 application-druid.yml 完整配置示例与参数位置RuoYi-Vue-Plus 的多数据源配置通常放在application-druid.yml里通过spring.profiles.include: druid加载。核心配置前缀是spring.datasource.dynamic不是spring.datasource.druid.dynamic这个层级关系要记牢。一个完整的双数据源配置如下spring: datasource: dynamic: primary: master strict: false druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false datasource: master: url: jdbc:mysql://localhost:3306/ruoyi_master?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://localhost:3306/ruoyi_slave?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: read_only_user password: read_only_pass driver-class-name: com.mysql.cj.jdbc.Driver逐个解析下关键参数。primary指定默认数据源所有不加注解的方法都会落到这个库上一般设成 master。strict建议在开发环境设成 false方便其他数据源暂时没配好时项目还能启动生产环境我更倾向于设成 true这样一旦程序里出现拼错的数据源 key会立刻抛异常而不是悄悄走了主库排查起来高效得多。druid节点下的是连接池全局参数如果你用的是 HikariCP需要把type改成com.zaxxer.hikari.HikariDataSource并且用hikari节点管理连接池参数。如果你只有单库可以只配置 master。但要注意代码里如果有人用了DataSource(DataSourceType.SLAVE)而 slave 不存在严格模式下会直接报错。所以单库项目的稳妥做法是让 slave 的 url 跟 master 指向同一个库省得后面新增业务时莫名其妙被这个坑绊倒。3. 在代码里切换数据源的三种正确姿势3.1 Service 类级切换适合整块业务走独立库最常用的切换姿势是把DataSource标在 Service 实现类上。类级别的注解会作用于该类的所有 public 方法适合“报表统计服务”“历史数据查询服务”这种整体走从库的场景。Service DataSource(DataSourceType.SLAVE) public class ReportStatisticsServiceImpl implements ReportStatisticsService { Resource private ReportStatisticsMapper reportStatisticsMapper; Override public ListStatisticsVO queryDailyReport(String date) { return reportStatisticsMapper.selectDailyReport(date); } }这段代码里queryDailyReport方法执行过程中所有数据库操作都会走 slave 数据源。主业务代码不需要做任何额外判断非常干净。要注意的是DataSource一定要标在 Spring 代理能拦截到的地方。类上的注解对 public 方法生效private 方法调用不会被 AOP 拦截因为代理对象无法切入私有方法。至于为什么我把注解放在实现类而不是接口上主要是 Spring AOP 对类上的注解解析最稳妥虽然动态数据源组件也支持从接口上找注解但没有必要给自己增加不确定性。3.2 Mapper 方法级切换细粒度控制有些场景不适合把整个 Service 都切过去。比如一个订单查询接口大部分基础信息在主库但某个字段的补充信息只在从库的历史表里这时候就要在 Mapper 方法上做细粒度切换。public interface ReportOrderMapper extends BaseMapperReportOrder { DataSource(DataSourceType.SLAVE) ListReportOrder selectSlaveOrders(Param(shopId) Long shopId); }方法级注解的优先级更高哪怕类已经标了 master这个方法也会走 slave。这一点在编写复杂业务时很实用同一个事务里大部分表操作走主库个别查询单独指定从库不会影响整体路由。需要注意的是不要在同一个类内部通过 this 调用另一个带 DataSource 的方法因为 this 调用不会经过代理AOP 拦截器根本不会执行注解自然失效。解决办法很简单把需要切换数据源的方法拆到另一个 Service 类里由 Spring 注入代理对象来调或者注入AopContext.currentProxy()走代理调用但那个方式可读性差我一般直接用拆类方案。3.3 手动切换与运行时验证注解切不掉的时候还可以手动切换。DynamicDataSourceContextHolder提供了 push 和 poll 方法适合在循环里切换、在没有 AOP 的代码块里临时切换等特殊场景。public ListMapString, Object queryWithManualSwitch() { DynamicDataSourceContextHolder.push(slave); try { // 这里的数据库操作都会走 slave return slaveMapper.selectAll(); } finally { DynamicDataSourceContextHolder.poll(); } }手动切换最大的隐患是忘了 poll。如果线程池中的线程执行到这里 push 了 slave 却因为异常没有弹出下一次这个线程被复用时上下文里还残留着 slave 的 key后续不该走从库的请求就会跑到从库上去。我见过不止一次线上事故是以这种诡异的方式出现的所以一定要用 try-finally 保证弹栈千万别图省事。验证当前数据源是否切换成功可以利用 ThreadLocal 里存的值String currentKey DynamicDataSourceContextHolder.peek();也可以注入DynamicRoutingDataSource直接看当前可用的数据源 key 集合Resource private DynamicRoutingDataSource dynamicRoutingDataSource; public SetString listAvailableDataSources() { return dynamicRoutingDataSource.getCurrentDataSources().keySet(); }更直观的验证方式是在两个库里分别执行不同的标识查询。比如主库写了一条config表的数据值为master_version从库同样的表里值为slave_version然后写两个接口分别带不带注解去查询看返回值是哪个。这种方式虽然原始但最能确认整条链路没有配错。4. 事务与多数据源最容易翻车的地方4.1 一个例子理解事务为什么“吃掉”数据源切换多数据源配置好后最早踩的坑大概率跟事务有关。先看这个典型错误Transactional public void syncOrder() { orderMapper.insert(order); // 当前线程绑定 master 连接 slaveOrderMapper.selectCount(); // 这里想切到 slave实际仍走 master }表面看slaveOrderMapper上可能标了DataSource(DataSourceType.SLAVE)但执行结果却还是落了主库甚至干脆报错。原因在于 Spring 事务的执行逻辑进入Transactional方法时事务管理器会提前从 DataSource 获取连接并把连接绑定到当前线程。多数据源切换的本质是“在获取连接的瞬间决定路由到哪个库”而事务一开始 Connection 就已经拿到了后续无论怎么 push 数据源 keySpring 拿到的都是已经绑定的那个连接切换自然不生效。打个比方你在订餐平台选好餐厅后订单已经生成这时候你再切换收货地址平台只会按原地址配送因为出餐流程已经锁定了。事务与连接的关系就是这个道理。很多教程说“把 DataSource 放在 Transactional 之前”能解决其实没说到根上真正的问题不在注解顺序而在连接的获取时机。4.2 三种可靠解法对比要解决事务内切换数据源失效的问题核心思路只有一个让切换数据源的代码在一个新的事务中重新获取连接。三个常用方案各有取舍我把对比整理成了表格。方案思路优点缺点适用场景拆 Service REQUIRES_NEW把要切换库的方法拆到独立 Service方法标注 REQUIRES_NEW 开启新事务切换可靠、代码结构清晰新事务独立提交失败不回滚外层从库操作失败可重试、业务容忍部分成功编程式事务 TransactionTemplate手动控制事务边界在事务块内自行拿到数据源连接灵活不依赖 AOP代码侵入性强容易写出重复样板复杂流程里需要精细控制连接获取时机去掉本地事务不开启事务直接让 DataSource 生效最简单数据一致性完全没有保障只读查询、报表统计最推荐的还是第一种。拆出来的独立方法DataSource和Transactional(propagation Propagation.REQUIRES_NEW)一起标注这样新事务开启时必须重新获取连接动态数据源才有机会介入路由。Service public class OrderSyncServiceImpl { Resource private SlaveOrderService slaveOrderService; Transactional public void sync() { orderMapper.insert(order); slaveOrderService.writeSlave(order); } } Service public class SlaveOrderServiceImpl { Transactional(propagation Propagation.REQUIRES_NEW) DataSource(DataSourceType.SLAVE) public void writeSlave(Order order) { slaveOrderMapper.insert(order); } }请注意writeSlave方法必须是通过 Spring 容器注入的SlaveOrderService来调用不能在同一个类内 this 调用原因前面说过AOP 切不到。4.3 跨库写入场景下的分布式事务思路如果业务是主库写完后从库也要同步写一份而且两者要求强一致那就不是加个REQUIRES_NEW能解决的事了。本地事务最多保证同一个数据库连接上的原子性跨库跨连接的事务已经超出了 Spring 事务管理器能管的范围。常见的出路是引入 Seata 的 AT 模式或 TCC再或者用本地消息表做最终一致。我对 RuoYi-Vue-Plus 项目里的建议是多数据源优先用于读写分离从库只读。读写分离下从库不涉及写入也就不存在跨库强一致的问题前面说的 REQUIRES_NEW 方案基本不会用到。如果一个业务需要同时写两个库先停下来想想能不能从模型层面合并比如把两个库的表放到同一个库里或者通过消息队列异步解耦。真要到了必须跨库强一致的地步再考虑 Seata但 Seata 引入后事务的复杂度会明显上升RuoYi-Vue-Plus 的 XA/AT 模式还需要额外配置全局事务 Group不是一两个注解能搞定的。5. 常见问题与排查技巧实录5.1 注解不生效仍然走 master这是出现频率最高的问题。我按排查顺序整理成了一张速查表照着走基本能定位症状可能原因解决方式加了 DataSource 但 SQL 仍打到主库strict 为 falsekey 拼错被静默回退核对 key 与 yml 中数据源名称一致临时改成 strict: true 验证同类内部 this 调用带注解的方法AOP 代理未介入拆分类或通过代理对象调用事务方法内切换失效Spring 事务已绑定连接拆独立 Service REQUIRES_NEW注解标在 private 方法上AOP 无法拦截私有方法改为 public并标到类或入口方法上数据源 key 写的是大写或带空格字符串比对严格区分大小写统一用常量或枚举不要手写字符串实际项目中我见过最多的情况就是strict: false掩盖了 key 拼写错误。这类问题的隐蔽之处在于系统正常运行、不报错但你查到的数据永远是主库的。我的习惯是在开发阶段就把strict设成 true宁可启动时报错也不要等到上线后才发现报表数据全是从主库查的。5.2 找不到数据源、启动失败启动时如果报Cannot find datasource by key: slave说明有人用了 slave 这个 key 但配置里没有 slave 数据源。RuoYi-Vue-Plus 的DataSource默认值就是 slave所以很多文件里没有显式写注解值结果一运行就报找不到。解决办法是检查 yml 里是否定义了 slave或者显式指定注解值。另一种常见的启动失败是动态数据源循环依赖这类问题通常发生在 Spring Boot 3 低版本 dynamic-datasource 的组合里。如果你在 pom 里看到 dynamic-datasource 版本低于 3.5.2 这种敏感区间优先升级到较新版本。排查时可以打开 Spring 的启动日志看循环依赖的 Bean 名称一般会指向DataSource初始化相关的类。还要提醒一下 yml 缩进问题。多数据源的配置层级比较深一旦缩进错了可能整个datasource节点都没被识别。如果反复确认代码没问题建议把配置里的 password 都先换成简单值排除特殊字符导致的解析问题。5.3 多数据源与 MyBatis-Plus 分页、Druid 监控的适配MyBatis-Plus 的分页插件在多数据源环境下默认是没问题的因为PaginationInnerInterceptor在执行 SQL 时会从当前连接自动推断数据库类型。但如果你搞了异构数据库比如主库 MySQL、从库 PostgreSQL那分页插件就需要显式指定DbType否则分页方言可能用错生成出语法怪异的 SQL。Druid 监控这块RuoYi-Vue-Plus 自带监控模块数据源较多时可以在 Druid 的配置里打开多数据源统计相关参数这样监控页面能分别展示 master 和 slave 各自的活跃连接数、执行 SQL 数。具体参数名以你使用的 dynamic-datasource 版本对应的文档为准不同版本写法略有差异。连接池参数也可以单独调从库连接数可以比主库大一些因为报表查询往往是长 SQL、慢查询连接占用时间久。5.4 快速定位当前数据源的实用手段排查问题时最直接的手段是看日志。在application.yml里打开动态数据源的路由日志级别logging: level: com.baomidou.dynamic.datasource: debug这样每次切换数据源时控制台会打印类似“find datasource by key slave”的日志可以清楚看到某个请求里到底走了哪个库。如果日志级别不方便调也可以在 Service 方法里临时打一句log.info(当前数据源: {}, DynamicDataSourceContextHolder.peek());还有一个我很常用的排查大招在 MySQL 里执行SELECT DATABASE()让 Mapper 把这个值返回出来就知道连接实际落在哪个库了。这个方式在测试环境尤其好用因为不依赖日志格式也不容易被连接池缓存误导。写在最后的一点个人经验多数据源配置本身并不难难的是约定和边界。我在实际项目里把多数据源用在订单主业务和报表历史库的分离上最大的体会就是从库一律只读跨库写入先拆独立事务。只要守住这两条底线绝大多数动态数据源问题都不会发生。还有一个小技巧数据源的 key 不要散落在业务代码里的各种字符串字面量中定义成常量类或者枚举统一管理配合 Nacos 这类配置中心动态调整数据源连接时你会感谢当初这个决定。RuoYi-Vue-Plus 的多数据源能力其实已经相当完善踩过几个坑后你会发现剩下的更多是业务设计层面的取舍而不是技术能不能做到的问题。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →