news 2026/10/20 7:44:28

MyBatis-Plus多数据源配置实战:从选型到避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis-Plus多数据源配置实战:从选型到避坑指南

做后端开发的,基本早晚都会碰上“一个项目同时连多个数据库”的需求。业务库、日志库、报表库、老系统数据,拆分之后都要在一个工程里访问,这就引出了 MyBatis-Plus 多数据源配置这件事。我第一次搞的时候,第一反应是像以前一样写多个 MyBatis 配置、配多个 SqlSessionFactory,结果环境一换配置文件直接失控,后来换成官方生态里的多数据源 starter,配置从十几行缩到几行,代码里加一个切换注解就够了。这篇文章我不会只贴配置,会把选型理由、坑点、排查思路一起说清楚,适合正打算从单数据源切换到多数据源,或者已经被切换问题折磨过的后台开发。

1. 为什么需要多数据源:不只是“切换”这么简单

1.1 真实的业务场景比我预想的多

我最早以为多数据源就是读写分离,后来在实际项目里观察了一圈,发现需求远不止这一种。

最常见的是读写分离,主库承担写入,从库承担读取,尤其是报表、查询、统计这类重读场景,单独把读流量打到从库上,能明显缓解主库压力。其次是业务模块隔离,比如用户中心和订单中心虽然在一个后端进程里,但数据库是两个库,原因可能是权限审批、数据归属、团队维护边界,也可能是历史遗留的难言之隐。还有一种是数据汇聚,几个业务系统各自有库,但上层需要一个统一的查询入口,把多个库的数据拉出来合并展示,这种场景下多数据源几乎是必然选择。

另外还有一种容易被忽略的:老系统不迁移。很多公司都有跑了好多年的旧库,新系统无法直接改表结构,只能通过多数据源去连老库做数据同步或联动查询。我见过一个项目,新系统的主库干干净净,业务接口每次还要去隔壁老库里查一份历史订单,然后做字段拼装。这种需求用多应用服务隔离当然也能做,但成本和部署复杂度都高了,一个进程内解决明显更轻。

1.2 三种主流方案,我为啥选它

多数据源的实现方式,业内其实早就有一套演进路径。最初是手动配多个 DataSource 和多个 SqlSessionFactory,让不同 Mapper 包路径对应不同库。这个方案能用,但配置非常啰嗦,每加一个库都要复制一坨类似的 Bean,而且代码里要按包名约定来区分,稍微不注意就绑错库。维护大半年后,光看配置就能看到几百行,没人愿意动。

第二种是 Spring 自带的AbstractRoutingDataSource。它本身只提供“在当前线程里选择目标数据源”的机制,路由规则要自己实现,通常是写一个上下文持有类加一个切面,拦截注解或者方法名去做选择。这个方案灵活性最高,适合完全掌控路由规则的团队,但对大多数人来说,相当于把轮子重新发明一遍,而且边界问题特别多,比如事务开启后路由失效、连接池缓存导致串库、线程池复用时上下文没清理等。

第三种就是 MyBatis-Plus 生态里的dynamic-datasource-spring-boot-starter。它本质上是基于AbstractRoutingDataSource做的封装,但把注解切换、分组路由、连接池适配、事务支持这些高频能力都内置好了。选它的核心原因有两点:一是新增数据源几乎不动代码,改 yml 就能扩展;二是团队协作时心智负担低,同事一看@DS("report")就知道这段代码走哪个库,不用去翻一坨配置类。

实现方式配置成本切换方式适用场景
多 SqlSessionFactory高按 Mapper 包隔离一次性冷启动,模块间极少共享
自研 AbstractRoutingDataSource中自己写路由和切面路由逻辑极其特殊,团队能力强
dynamic-datasource-starter低注解 + 分组绝大多数业务项目

如果你是自己写路由方案,我建议先想清楚三件事:异常回滚时数据源上下文怎么恢复、多线程异步任务里数据源怎么传递、事务内切换数据源是否允许。这三个问题在 starter 里都有成熟答案,自己写的话每个都是坑。

2. 准备工作:依赖、版本与连接池

2.1 依赖怎么加才不出错

项目本身已经在用 MyBatis-Plus,动态数据源只需要多引入一个包。Maven 工程里加这一段:

<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>4.3.0</version> </dependency>

版本这里要多说一句。4.x 系列对应 Spring Boot 3.x,3.x 系列对应 Spring Boot 2.x,两个大版本不能混用,否则启动时会各种类加载异常。很多人看到“最新版本”就无脑升,结果项目还在 Spring Boot 2.7,引入了 4.x 的包,服务直接起不来。建议直接去官方文档页看版本说明,按照自己项目的 Spring Boot 大版本选对应的 starter 版本。

引入这个 starter 之后,原来的 MyBatis-Plus 配置基本不用动。动态数据源会自动成为 MyBatis 的数据源,底层管理所有真实数据源。有个细节要注意:如果你的项目突然出现@DS不生效,先别怀疑配置格式,优先检查是不是用 Spring Boot 3 但 aop 依赖被排掉了。再加上spring-boot-starter-aop一般能解决。

2.2 连接池别乱选

dynamic-datasource 默认使用 HikariCP,如果你不额外引入 Druid,它就是你的连接池。如果项目里已经有druid-spring-boot-starter,组件会自动感知并适配 Druid 的配置方式。这里我的建议很直接:需要监控、慢 SQL、防火墙这类能力就上 Druid,内部小项目、追求轻量和稳定用默认的 HikariCP 就够了。

连接池最忌讳的是混用。我见过一个 pom 里同时引入 Hikari 相关配置又引了 Druid starter 的,结果两个连接池的初始化互相干扰,出现“明明配置了最大连接数,实际却用了另一个池子的参数”这种诡异现象。排查起来非常痛苦。一旦确定用某个连接池,就把另一个 starter 从依赖里拿干净,包括传递依赖也要排查。

还有一个容易忽略的点:多个数据源各自维护自己的连接池,连接池参数也是独立的。如果只配置了一个spring.datasource.druid.max-active,它不会自动应用到所有动态数据源上,需要按数据源维度去写,或者用全局配置再单独覆盖。

3. 多数据源的配置:主库、从库与分组

3.1 最基础的配置长这样

用 yml 做多数据源配置是最直观的:

spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://127.0.0.1:3306/app_main?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver report: url: jdbc:mysql://127.0.0.1:3306/app_report?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: read_user password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

这个配置里的primary: master指定了默认数据源,也就是没有@DS注解时走的数据源。strict: false表示@DS里如果写了不存在的库名,不会抛异常,而是回退到primary数据源;如果设为true,一旦匹配不到就直接报错。

配置项命名有一个容易被忽略的规则:数据源名称只能使用小写字母、数字、下划线和中划线,不要带点。有人习惯把配置写成order.db,结果启动阶段数据源解析就出问题。另一个建议是显式声明driver-class-name,特别是多个数据库类型混用时。如果全是 MySQL,不写也许能自动识别,但一旦混入 PostgreSQL 或 Oracle,自动识别就会翻车。

3.2 同组数据源:读写分离的核心玩法

日常项目里用得最多的不是一堆完全独立的库,而是“一个主库多个从库”的结构。这种情况不需要让调用方感知从库的每一个节点,可以用分组把它们包起来:

spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://127.0.0.1:3306/app_main username: root password: 123456 slave_1: url: jdbc:mysql://127.0.0.1:3306/app_main_slave_1 username: read_user password: 123456 slave_2: url: jdbc:mysql://127.0.0.1:3306/app_main_slave_2 username: read_user password: 123456 group: slave: - slave_1 - slave_2

配置了group之后,代码里只需要写@DS("slave"),组件会自动在slave_1和slave_2之间做负载均衡,默认策略是轮询。以后要扩容从库,只需要在 yml 里加一个slave_3并把它加进 group 列表,Java 代码一行都不用改。这个设计对运维非常友好,也是我在项目里最常用的一种配置方式。

3.3 strict 模式的取舍

strict 是一个值得掰开说的参数。默认strict: false的好处是容错,正常情况下不影响使用,但代价是拼错数据源名不会立刻报错,而是默默走主库。比如把report拼成reprot,查询没有走报表库,而是打到了主库,数据没查到还算小事,如果是个写操作,可能就写错库了。

我的习惯是:开发环境用strict: false减少启动干扰,生产环境尽量开成strict: true,让潜在错误早点暴露。如果团队里服务很多,还可以考虑写一个配置检查脚本,启动时扫描所有@DS注解的值,和 yml 里的数据源名称做比对,漏配、错配直接 fail。这个对大型团队非常实用,因为人总是会犯低级错误。

4. 数据源注解的用法与切换细节

4.1 类级别与方法级别怎么选

@DS注解是整套方案最核心的使用入口。它可以放在类上,也可以放在方法上,关系很简单:方法上的注解优先级高于类上的,如果没有方法注解就用类上的,都没有就走 primary。

@Service public class OrderService { // 当前方法使用报表库 @DS("report") public List<ReportData> queryReport() { return reportMapper.selectList(...); } // 未标注时,该 Service 类上如果有 @DS 就用类上的,没有就走默认主库 public void saveOrder(Order order) { orderMapper.insert(order); } }

放到类级别,适合整个类都只访问同一个库的情况。比如一个专门做报表查询的ReportQueryService,里面所有方法都走报表库,类上标一个@DS("report")就行,不用每个方法都写。放到方法级别,适合同一个 Service 里有的方法走主库、有的方法走从库的混合场景。

有个容易被忽略的细节:如果同类内部方法之间互相调用,@DS是无效的。因为 Spring 的 AOP 代理只对从外部进入的方法生效,方法内部this调用走的是当前对象,不会经过代理。如果确实要在内部强制切换,可以考虑注入自身的代理对象,或者把不同数据源的方法拆到不同 Bean 里。这个特性和 Spring 事务是同一个道理。

4.2 方法内部动态切换该怎么做

注解适合“方法的库是确定的”情况,但有些场景是运行时才知道查哪个库。比如传入一个用户编号,要根据编号路由到不同的分库;或者接口里带了个dbName参数,需要动态选择数据源。这种时候就得编程式切换:

public List<Order> queryByDatabase(String dbName, Long userId) { DynamicDataSourceContextHolder.push(dbName); try { return orderMapper.selectByUserId(userId); } finally { DynamicDataSourceContextHolder.poll(); } }

push是压入数据源标识,poll是弹出并恢复上一个标识。这里最最关键的是一定要写在finally里,确保无论正常返回还是抛异常,上下文都能清理干净。线程池里的线程是复用的,如果poll漏掉了,当前线程下一次被分配到别的请求时,仍然带着上一次的数据源 key,出现传说中的“串库”问题。

这种串库问题在线上非常隐蔽,因为不是每次都必现,只有在并发高、线程复用频繁的时候偶发。我一个同事排查了整整一天,最后发现是一个try catch里只处理了正常逻辑,异常路径忘了poll。所以能用注解解决就不要手写上下文,手写了就一定要保证成对出现。

4.3 Mapper 层有没有必要加 @DS

有人图省事,直接在 Mapper 接口或 Mapper 方法上加@DS。这个做法能用,但我一般不建议作为主入口。原因很简单:数据源切换应该发生在业务边界上,而不是 SQL 边界上。

一个业务方法可能会调用多个 Mapper,如果每个 Mapper 上都标了不同的数据源,那函数的整体一致性就没了。比如在一个 Service 方法里,先查询主库的用户,再查询报表库的统计,然后根据结果执行某个写入操作,如果注解只标在 Mapper 上,最后写入时走的是哪个库,完全取决于那个 Mapper 有没有标@DS,调用链一长就非常混乱。

我更推荐的做法是:

  • 大部分情况下,把@DS放在 Service 实现方法上,让一次业务请求完整跑在一个数据源上。
  • 确实有单条 SQL 需要跨库执行的特殊场景,才在 Mapper 方法上加,同时写清楚注释。
  • 不要在 Mapper 接口上写类级别@DS,容易让数据源归属变得零散,后来维护的人完全无法从调用代码看出当前在操作哪个库。

5. 事务与多数据源:最容易翻车的组合

5.1 事务注解和数据源注解谁先生效

这是整个多数据源配置里翻车率最高的话题。很多人会把@Transactional和@DS("report")放在同一个方法上,以为只要两个注解都在,事务里的所有 SQL 都会走报表库。实际结果往往不是这样。

先看一个现象:一个 Service 方法加了@Transactional,方法内部调用了另一个加了@DS("report")的方法,执行时 SQL 仍然打到了主库。这不是注解失效,而是事务开启后,连接就已经从数据源获取并绑定到当前线程,后续再切换数据源 key,并不会把当前事务里已经持有的连接换掉。动态数据源组件能控制的是“下次获取连接时选择哪个数据源”,控制不了已经绑定在事务里的连接。

所以实际经验是:

  • 如果@DS和@Transactional在同一个方法上,并且@DS能在外层完成数据源选择,那么整个事务可以正常跑在指定数据源上。
  • 如果事务方法内部再调用一个@DS方法试图切换,基本无效。
  • 如果一个事务方法里涉及多个数据源的写操作,不要指望本地事务能覆盖所有库。

这也意味着业务上应该尽量一个事务只操作一个数据源。如果确实需要在一次请求里先写主库再写从库,两边又要保持强一致,本地事务解决不了,必须考虑分布式方案或者重新设计一致性边界。

5.2 跨库强一致怎么做

说到跨库强一致,大家首先想到 Seata。Seata 的 AT 模式在代码层面侵入很小,配合动态数据源也有适配方案,但它会引入全局事务协调器,性能损耗和运维成本都不低。不是每一个跨库场景都需要这种强一致。

我的判断标准很简单:如果业务允许最终一致,优先选择本地消息表或者基于消息队列的事务消息,把跨库操作拆成“主流程 + 异步补偿”。只有真正核心交易链路,比如资金、订单状态这类不能容忍中间状态的地方,再考虑全局事务。还有一个折中方案是把需要强一致的操作尽量收敛到一个库,通过冗余字段或者同步机制把另一个库的写入延后,避免同时跨两个库的事务。

如果你的团队没有专人维护分布式事务基础设施,我个人建议不要在初期就引入 Seata。多数据源本身解决的是“访问多个库”的问题,事务的一致性边界属于另一个层面的架构设计,两者混在一起处理,复杂度会成倍增长。

6. 常见问题与排查技巧实录

6.1 高频问题速查表

我把这些年遇到的多数据源问题整理成了一张表,基本覆盖了最常见的坑。

现象最常见原因处理方式
@DS 完全不生效starter 版本与 Spring Boot 不匹配,或 AOP 未启用检查版本对应关系,引入 spring-boot-starter-aop
切换后走了默认库strict 为 false,且 @DS 数据源名拼错先开 strict=true 复现,再核对数据源名称
线程复用后串库手写 push 后没有 finally poll用 try-finally 保证恢复,或用注解切换
事务内切换不生效事务已绑定连接不要在事务内切换数据源,调整注解位置
连接池配置不生效引入了多个连接池 starter只保留一个连接池依赖
启动时找不到主数据源primary 名称配置错了或主库被删确认 primary 指向的库一定存在
大事务下偶尔使用旧连接连接池归还时上下文未清理检查所有编程式切换的 finally

这张表不是纸上谈兵,每一行都对应过真实的报错现场。尤其是“切换后走了默认库”,新手往往会盯着代码找半天,其实只要把strict临时改成true,启动后立刻就能看到是哪个名字没匹配上。

6.2 定位数据源问题的三个实用招

排查多数据源问题时,最傻但最有效的办法就是看实际 SQL 打到了哪个库。具体我习惯按三步走:

第一步,打开 SQL 日志。MyBatis 的日志里会打印 SQL 语句,但要确认执行在哪个库,最好再开启对应数据库的慢查询日志或者 general log。如果用的是 Druid,可以直接在监控页面看到连接来源。

第二步,打印当前线程的数据源上下文。在疑似出错的位置加一行日志:

logger.info("current datasource key = {}", DynamicDataSourceContextHolder.peek());

peek()可以查看当前线程的数据源 key,且不会弹出栈。把它放在 Mapper 调用前后,就能定位到 SQL 执行时到底是哪个数据源被选中。

第三步,临时把strict设为true。这个方法见效最快,只保留一个@DS注解,启动服务,如果配置匹配不上,组件会直接抛出异常,异常信息里会列出当前可用的数据源列表。这比自己对着 yml 猜名字高效得多,尤其是数据源名称特别多的时候。

最后一招比较极端:把连接池的最大连接数调成 1,或者为每个数据源单独指定不同的连接池参数,通过观察连接池状态来判断实际创建连接的路由目标。这在多实例、负载均衡场景下特别有用。

6.3 几个外部文档不会写的细节

有几个细节我是在实际项目里踩过之后才真正明白的。

第一,不要在一个 pom 里同时引入多个连接池的 starter。之前提过,这个坑值得单独列出来。动态数据源自动配置会按顺序选择连接池,但另一个连接池的初始化配置可能被跳过,导致你看到“连接失败”或者“参数没生效”。清理依赖以后,问题往往立刻消失。

第二,数据源命名一定要统一。如果多个团队共建一个工程,建议在代码规范里定义一个数据源名称枚举,而不是让每个人随手写字符串。否则时间一长,代码里会出现@DS("report")、@DS("Report")、@DS("rep")三种写法,只有第一种能匹配上。

第三,动态数据源切换本身只影响连接获取,不影响 MyBatis-Plus 里的其他插件,比如分页插件、乐观锁插件。这些插件绑定的是 SqlSessionFactory,而 SqlSessionFactory 绑定的是动态数据源代理,所以插件不用额外配置。很多人加了多数据源后担心分页失效,实测没有这个问题。

第四,primary指定的主库不要轻易改名。它是整个数据源体系的兜底,如果代码里大量方法没标@DS,改名等于让所有默认操作换一个库,风险非常大。新增数据源可以通过添加配置实现,但已有主库的调整一定要谨慎推进。

最后再分享一个我自己的维护习惯。每次接手一个多数据源项目,第一件事就是把 yml 里的数据源清单和代码里用到的@DS名称整理成一张对照表。服务一多,光靠人脑记是记不住的。新库上线时按表新增配置就行,不要顺手改旧名字,因为历史代码里的字符串散落各处,改一个名字等于把所有相关代码、日志、告警规则都跟着更新一遍。这套方法让我在复杂多数据源路由场景下维护了大半年,整体还算轻松,希望也能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 7:43:45

AI Agent开发陷阱:为什么最该做的是停止而不是继续

你有没有过这种经历&#xff1a;一个项目做久了&#xff0c;明明已经感觉不对劲&#xff0c;却还是硬着头皮往下写&#xff1f;哪怕心里清楚大概率没有未来&#xff0c;却总抱着“再改一版就行”“还差最后一个功能”的念头。我花了几个月时间做 AI Agent&#xff0c;到最后复盘…

作者头像 李华
网站建设 2026/10/20 7:43:43

基于单片机的无接触红外测温系统设计与实现

红外测温枪如今已经是随处可见的日常设备&#xff0c;商场入口、医院门诊、小区门卫都用它来快速完成体温筛查。但测温枪是成品设备&#xff0c;如果需要在某个具体场景里定制一套无接触测温装置&#xff0c;或者学生要完成一个相关的课程设计&#xff0c;那台面上的PCB、单片机…

作者头像 李华
网站建设 2026/10/20 7:43:15

Hadoop YARN AsyncDispatcher源码解析:事件驱动与问题排查指南

啃到 Hadoop YARN 源码的这个阶段&#xff0c;你大概率已经对 ResourceManager、NodeManager 这些大骨架有了概念。但真正让整个集群状态像流水线一样转起来的&#xff0c;其实是藏在org.apache.hadoop.yarn.event包里的 AsyncDispatcher——也就是 YARN 事件异步分发器。本系列…

作者头像 李华
网站建设 2026/10/20 7:42:48

大模型面试高频问题详解:Transformer、RAG与Agent应用实战

“大模型面试必备&#xff1a;小白程序员必看&#xff0c;收藏这些高频问题轻松拿下Offer&#xff01;”这两年大模型岗位的面试难度&#xff0c;说实话有点两极分化。一边是算法岗卷到飞起&#xff0c;论文、比赛、实习经历缺一不可&#xff1b;另一边是应用开发、工程落地、P…

作者头像 李华
网站建设 2026/10/20 7:42:05

从工具焦虑到组织赋能:AI学习进阶路径与实践指南

1. 先看清“工具焦虑”的本质&#xff1a;你不是学不会&#xff0c;是路径没拉开我见过太多人卡在同一个地方&#xff1a;今天听说某某大模型出了新功能&#xff0c;赶紧去试&#xff1b;明天看到别人用AI做了一个很炫的Demo&#xff0c;又觉得自己落后了&#xff1b;后天刷到一…

作者头像 李华
网站建设 2026/10/20 7:41:19

微信小程序餐饮点餐开箱即用:本地mock+沙箱支付闭环

简介&#xff1a;这是一套开箱即用的微信小程序餐饮点餐外卖系统源码&#xff0c;面向前端开发者、小程序初学者及中小型餐饮商家技术负责人&#xff0c;旨在快速搭建具备点餐、外卖、堂食叫号、微信支付等核心功能的轻量级线上服务。资源包共521个文件&#xff0c;涵盖137个JS…

作者头像 李华