ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MyBatis-Plus多数据源配置实战:原理、踩坑与最佳实践

2026/10/3 18:39:47 拓冰建站 浏览量
MyBatis-Plus多数据源配置实战:原理、踩坑与最佳实践 做多数据源这件事我前前后后折腾过好几个方案从最开始手动写AbstractRoutingDataSource到后来切到mybatis-plus的dynamic-datasource最大的感受就是如果项目里已经用了mybatis-plus多数据源配置真的没必要自己造轮子。这个starter把最脏最累的活都干完了而且坑也填得差不多了。这篇文章就把我的完整配置过程、踩过的坑、以及一些网上很少说清楚的细节一次性讲透保证你照着做就能跑起来。为什么项目会需要多数据源先别急着上手配置你得先想清楚一个问题你到底是不是真的需要多数据源我见过不少项目其实只有一个库但开发同学提前把多数据源的架子搭好了。这种“未雨绸缪”我一般不建议因为多数据源会引入额外的复杂度尤其是事务边界变模糊、连接管理变复杂一旦出了问题排查成本很高。真正需要多数据源的场景大致就这么几类读写分离主库负责写从库负责读这是最常见的诉求。分库分表的过渡阶段还没上ShardingSphere这种中间件先用多数据源顶着。业务数据隔离比如订单库、用户库、日志库物理上分离各有各的连接池。第三方系统对接需要直连别人家的数据库人家只给你只读账号。报表统计场景大查询走独立的库免得把业务库拖垮。我之前遇到的一个典型场景就是读写分离线上订单系统主库的压力还好但后台的报表查询全是那种多表关联的大SQL一跑就是好几秒直接把主库的CPU干到80%。后来就是把报表查询全部切到只读从库主库瞬间就稳了。这种场景用mybatis-plus的多数据源就是一两行注解的事收益非常直接。再说说为什么选mybatis-plus而不是自己写或者用Spring自带的方案。其实Spring的AbstractRoutingDataSource本身就能实现动态数据源我早年也这么干过得自己写AOP切面、自己维护数据源上下文、自己处理事务的绑定问题。说白了技术上能做但代码量不小而且边界情况特别多。mybatis-plus的dynamic-datasource-spring-boot-starter把这些封装好了还额外支持了嵌套切换、懒加载数据源、连接池监控这些实用功能。既然项目里已经用了mybatis-plus那直接用这个starter是最平滑的路子。动态数据源的核心原理用mybatis-plus配置多数据源很多人上来就抄配置跑通了就完事。但你要是不理解它背后的原理遇到问题的时候会非常痛苦。这个starter的原理一句话就能说清它本质上还是基于Spring的AbstractRoutingDataSource在每次数据库操作前通过AOP拦截DS注解把当前要用的数据源标识存到ThreadLocal里然后在真正获取连接的时候动态路由到对应的数据源上。我展开说一下这条链路你理解了以后排错会容易很多。先说底层的数据源路由。Spring提供了一个抽象类叫AbstractRoutingDataSource它维护了一个Mapkey是数据源标识value是实际的数据源对象。它有一个determineCurrentLookupKey()方法你重写这个方法返回一个keySpring在每次获取数据库连接的时候都会调它根据key取对应的数据源。mybatis-plus的dynamic-datasource对这个类做了增强如果当前线程没设置数据源标识就用默认主库如果设置了就用对应的库。这就是整个多数据源能跑起来的地基。再说AOP拦截。dynamic-datasource定义了一个切面拦截所有加了DS注解的方法。在方法执行前它会把DS里指定的数据源名称存到DynamicDataSourceContextHolder的ThreadLocal里方法执行完再把这个值清理掉。所以**DS注解的作用范围就是当前线程内的方法执行期**方法结束ThreadLocal就清了不会串到下一个请求。这里有个非常关键的细节DS注解是加在方法上的不是加在类上全局生效的。虽然注解本身支持加在Service类上但那也只是给这个类的所有方法都套了同一个数据源本质上还是方法级别的拦截。很多人误以为加在类上就是“这个类永远走某个库”其实不是事务和嵌套调用都会改变实际路由结果。理解了原理你再看配置里的每一行就都看得懂了。比如yml里的primary: master就是决定当ThreadLocal里没值的时候默认走哪个库strict: true是当DS指定了一个不存在的库名时直接报错防止你写错名字后静默走了主库这个务必要开。mybatis-plus的多数据源starter还做了一些深层的适配比如数据源懒加载也就是在Spring容器启动的时候并不立即创建所有数据源连接池而是等第一次用到某个库的时候才初始化。你如果配了很多个库这个特性会让启动速度快不少。另外它还内置了Druid、HikariCP的监控支持不过这个看你实际用什么连接池Druid配合的监控面板用起来更顺手一些。开箱即用完整配置步骤接下来就是实战环节。我尽量把每一步都写清楚包括依赖版本、配置文件、代码位置你照着做就行。先说环境SpringBoot 2.7.18mybatis-plus-boot-starter 3.5.7JDK 8。高版本的SpringBoot 3.x我也用过配置方式基本一致就是javax换成jakarta注意一下就行。第一步引入依赖在pom.xml里加入dynamic-datasource的starter。注意这个starter是独立的和mybatis-plus的starter是两回事别搞混了。!-- mybatis-plus 核心starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency !-- mybatis-plus 多数据源starter -- dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.6.1/version /dependency版本这里我多说一句3.6.1是目前比较稳定的版本3.5.x和3.6.x都有一些迭代但如果你项目里已经有mybatis-plus了建议让多数据源starter和mybatis-plus保持相近版本避免出现兼容问题。我踩过一次坑mybatis-plus用的3.5.3多数据源starter用的3.5.0结果是DS注解在某些场景下不生效排查了半天升级版本后就好了。第二步配置yml数据源在application.yml里配置数据源信息。这个是核心配置spring: datasource: dynamic: primary: master strict: true strategy: com.baomidou.dynamic.datasource.DynamicDataSourceStrategy datasource: master: url: jdbc:mysql://192.168.1.10:3306/order_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://192.168.1.11:3306/order_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: readonly_user password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver report: url: jdbc:postgresql://192.168.1.12:5432/report_db username: report password: 123456 driver-class-name: org.postgresql.Driver几个关键点解释一下primary: master默认数据源所有没加DS注解的Mapper操作都会走这个库。这是兜底的我建议永远保留一个master别把主库配成别的名字。strict: true严格模式。如果DS里写了一个不存在的库名直接抛异常不会静默走主库。这个建议开启否则你拼错一个字母数据写到主库里去排查的时候想哭的心都有。数据源名称你可以随便起比如master、slave、report但注意这些名字会在DS注解里引用要保持一致。第三步在启动类排除DataSourceAutoConfiguration这一步很多人会漏掉但关键程度不亚于前面的配置。如果你使用的是SpringBoot 2.x以上版本启动类要加这个排除SpringBootApplication(exclude DataSourceAutoConfiguration.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }为什么要排除因为SpringBoot的自动配置会读到spring.datasource.url之类的配置去创建DataSource你现在的配置是放在spring.datasource.dynamic.datasource.master下面的SpringBoot默认配置根本读不到就会报错说找不到主数据源。dynamic-datasource的starter自己会创建一个动态数据源所以必须把SpringBoot默认的数据源自动配置排除掉。这一步网上很多教程没提或者提了没解释为什么。实际上一开始我没加这个排除启动直接报错Failed to configure a DataSource: url attribute is not specified加了排除以后马上就好了。第四步写测试代码验证配置完成后写一个简单的Service来验证效果。DS注解加在Service层的方法上或者直接加在Mapper的方法上。我建议加在Service层因为业务上你更关心的是“这个业务方法走哪个库”而且Mapper层面太细了不好管理。Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public User getMasterUser(Long id) { return userMapper.selectById(id); } DS(slave) Override public User getSlaveUser(Long id) { return userMapper.selectById(id); } DS(report) Override public ListReport getReportList() { return reportMapper.selectList(null); } }这里有个非常关键的区别getMasterUser方法没加DS走的就是primary指定的主库getSlaveUser加了DS(slave)走的就是从库getReportList走的是report库。注意DS注解可以加在类上表示这个类所有方法都走同一个库如果类上和方法上都有DS以方法上的为准方法级的优先级更高。从库的UserMapper和主库的UserMapper其实是同一个Java接口同一个实体类。区别只在于你调用时走的方法加没加注解。这一点就体现了dynamic-datasource的设计精髓一个Mapper可以自由穿梭在多个数据源之间。高版本SpringBoot的额外注意事项如果你用的是SpringBoot 3.x也就是JDK 17的环境配置会有一个额外的变化。除了刚才说的排除DataSourceAutoConfiguration还得注意数据库驱动的坐标变化dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencyMyBatis、Druid这些组件在Boot 3.x下都需要对应新版本。dynamic-datasource 3.6.1本身就兼容SpringBoot 3.x这个倒不用担心。还有一点SpringBoot 3.x里用的是Jakarta命名空间如果项目里引了老的javax依赖会直接起不来。高版本还有个常见的坑是数据源初始化顺序的问题。SpringBoot 3.x对自动配置的顺序要求更严格如果你发现启动报错说什么DataSource被循环依赖了试试在启动类上把SpringBoot的DataSourceTransactionManagerAutoConfiguration也排除掉SpringBootApplication(exclude { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })不过要注意排除掉DataSourceTransactionManagerAutoConfiguration之后事务管理器的自动配置就没了需要你自己注册或者直接用dynamic-datasource提供的事务管理器。这个坑一般出现在你同时用了Spring的Transactional和DS的场景后面我会详细说。事务和数据源切换的那点事儿这是多数据源配置里最让人头疼的部分我放在这里单独讲。当你用Transactional的时候事务是在方法进入前就开启了连接也是在事务开始时从默认数据源拿的。如果这个事务方法内部又调用了带DS的方法那DS可能根本不会切换数据源或者切换了也不生效。看下面的例子Transactional public void transfer() { orderMapper.insert(order); // 走主库 getSlaveUser(1L); // 想切到从库但可能不生效 }问题出在Transactional开启事务时已经把连接和事务绑定到当前线程了DS的AOP切面在事务方法内部执行时试图去切换数据源但事务管理器还是拿着旧连接不放。所以事务内的数据源切换会失效或者报错。我实测过的结果是如果你的transfer()方法上加了Transactional内部再调getSlaveUser()实际查询还是会走主库。这不是bug而是Spring事务机制的固有行为。解决办法有几个思路把不需要走主库的操作拆出去放到一个独立的事务方法里这个方法单独加DS和Transactional。使用dynamic-datasource的DSTransactional来代替Spring的Transactional它支持多数据源下的事务管理但它是本地事务不支持跨库的分布式事务。如果确实需要跨库强一致那你需要引入Seata这类分布式事务中间件这是另一个范畴了本文不展开。特别注意多数据源配置开启了以后Spring自带的Transactional默认绑定的是主数据源的事务管理器。如果你在从库上做写操作并加了Transactional事务并不会真正在从库上生效这个细节很多人踩坑后才回过神来。这一点非常容易忽略务必注意。我个人的习惯是从库只做查询不开放写权限数据库账号也不给写权限。这样从根本上避免从库上出现写事务。实操中遇到的坑和排查心得配置跑通之后还有一堆实际问题。我把这些坑按频率从高到低列出来你遇到的时候可以直接对号入座。坑1DS注解不生效表现是有DS注解的方法实际还是走的默认库。排查思路先确认starter版本是否兼容然后确认方法是否被Spring代理。如果你是在一个类内部的this.xxx()调用那DS注解是不会被AOP拦截的。因为Spring的AOP基于代理this调用不走代理对象。Service public class OrderService { // 这样调用DS会失效 public void doSomething() { this.queryFromSlave(); } DS(slave) public void queryFromSlave() { // ... } }解决办法是通过注入自己的代理对象或者把方法拆到另一个Service里调用。这个是最常见的DS不生效原因没有之一。坑2连接池耗尽或者连接泄漏多数据源意味着多个连接池每个都是独立的默认的连接池大小可能不够。如果你同时有主库、从库、报表库高峰期很容易出现某个库的连接池被打满的提示。而且如果你手写代码时手动拿了连接没有释放那个连接池就泄漏一个时间长了就满了。建议给每个连接池设置合理的上限spring: datasource: dynamic: datasource: master: hikari: maximum-pool-size: 30 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 slave: hikari: maximum-pool-size: 20 minimum-idle: 5另外注意从库的连接池峰值往往更高因为读请求多把从库的maximum-pool-size调大一点是合理的。我之前就是主库30、从库50这么配的。坑3MyBatis二级缓存串库如果你开了MyBatis的二级缓存缓存的是查询结果但二级缓存的key是Mapper的namespace不区分数据源。这意味着你用主库查了一次缓存的键没有数据源维度同类查询切到从库时命中的可能是主库的缓存结果。这个问题在小项目里可能不出事但一旦两个库的数据不一致就会出诡异的问题。建议多数据源场景下直接关闭二级缓存老老实实用一级缓存和Redis。坑4主从延迟导致的数据不一致读写分离的场景下从库的数据有延迟。你刚写完主库马上去查从库可能查不到刚写的数据。这时候需要区分业务场景对实时性要求高的查询强制走主库能容忍延迟的就放心走从库。比如订单详情页的查询建议直接走主库或者做一个短时间的缓存别让主从延迟影响用户体验。这个问题跟多数据源本身无关但既然做了读写分离就一定会遇到提前了解解法是很有必要的。坑5连接在事务方法内部切换导致连接管理器报错这个场景其实比较偏但我确实遇到过。动态数据源里如果在一个事务执行途中切换数据源DynamicRoutingDataSource会尝试在当前线程重新绑定连接而旧连接和事务的关系会变得很微妙进而出现一些奇怪的SQL执行错误日志里会蹦出来类似Connection is not available这种。所以再次强调不要在一个事务方法里来回切换数据源要么拆分方法要么全走多数据源事务注解。多数据源常见问题排查速查表我整理了一下实战中比较高频的场景做成一个速查表方便你遇到问题时快速定位方向。问题现象可能原因解决思路启动报错url attribute is not specified没排除DataSourceAutoConfiguration启动类排除该自动配置DS注解完全不生效类内部this调用没走代理拆到独立Bean或注入代理对象事务内部切换数据源失败Transactional在DS之前开启连接已绑定拆分事务方法或使用DSTransactional从库数据查不到刚写入的数据主从同步延迟实时性要求高的查询走主库连接池大量超时连接泄漏或连接池太小排查泄漏点调整Hikari参数查询结果串库MyBatis二级缓存不区分数据源关闭二级缓存strict模式下报数据源不存在DS注解写错库名核对yml里的数据源名称这个速查表不敢说覆盖所有情况但大部分新手遇到的就这些了。真正高级的问题得靠日志和监控一步步定位比如Druid的监控页面可以看到每个数据源当前的活跃连接数遇到连接问题看那个是最直观的。多数据源场景下的最佳实践建议最后聊几点实战心得。这些不是配置层面的东西而是架构层面的取舍但我觉得比配置本身更重要。能用主从复制解决的别用多数据源。如果你只是想减轻主库压力优先考虑从库复制然后在业务代码里通过切面或Mapper注解走从库。如果业务模块之间的依赖并不紧密干脆拆库拆服务走微服务每个服务管自己的库这才是彻底的解耦。配置层面建议每个数据源的最小空闲连接数不要太小即使流量不大也至少维持3到5个空闲连接避免突然的流量尖峰把连接池打满。连接池初始化是耗时的如果空闲连接全被回收了高峰时突然新建连接会产生明显的毛刺延迟。还有一个很多人忽略的管理问题多数据源的密码保管和变更。三个库的密码分别写在yml里每过几个月密码就会过期改起来虽然不难但容易漏。我之前就漏改了一个报表库的密码导致报表服务在凌晨定时任务跑了两个小时全失败了。建议引入配置中心来统一管理或者至少把数据源配置抽到独立的profile文件里别所有环境混成一个yml。最后说一个我比较坚持的原则生产环境开启strict模式任何SQL都不允许因为写错名字而静默落到默认库上。宁可让错误快速暴露宁可让自己半夜被电话叫醒也不要在用户页面出现数据错乱后去追查好几天的库。这是我吃了亏之后才长记性的。