资讯动态

Mybatis-Plus整合Druid多数据源配置:动态路由与事务实践

发布时间:2026/10/18 3:01:30 来源:尧图企业网站定制
先说结论如果你的项目里用了Mybatis-Plus又把Druid当连接池那多数据源这件事其实已经解决了一大半。真正麻烦的从来不是“配两个数据源”而是配完之后事务怎么管、Mapper怎么扫、SQL路由怎么切得干净利落。这篇就围绕“Mybatis-Plus搭建Druid多数据源配置”这个话题把设计思路、核心配置、动态路由原理、事务坑点、排查技巧一次说透顺便把我自己实际踩过的几个坑也交代清楚。1. 整体设计思路与数据源方案选型1.1 什么场景才会用到多数据源先别急着抄配置得先想清楚你到底需不需要多数据源。我见过不少项目其实只有一个库但为了“以后可能要分库”提前把多数据源的架子搭上了结果每次上线都要多排查一层问题纯粹给自己加戏。真正需要多数据源的场景无非这么几类读写分离主库写、从库读、多业务库并存比如订单库和用户库分开、数据迁移期间新旧库并行、以及报表查询走独立库不影响线上业务。这几类场景对数据源切换的诉求不太一样读写分离要求按方法或按SQL类型自动路由多业务库则更看重手动指定数据源的能力。如果你确认属于其中一类才有必要继续往下看。多数据源不是越早引入越好而是越需要才越值得引入。我建议按“业务需求倒推方案”的方式来做选型而不是因为某个框架支持多数据源就硬上。1.2 为什么选Mybatis-Plus Druid这套组合市面上的选择其实很多Spring自带AbstractRoutingDataSource可以做动态数据源ShardingSphere-JDBC也可以管理多数据源还有专门的dynamic-datasource-spring-boot-starter配合Mybatis-Plus和Druid使用非常顺。实际上很多团队在用的就是“Mybatis-Plus Druid dynamic-datasource”这三件套。我个人的理解是Mybatis-Plus的价值不只是CRUD简化它的分页插件、Mapper扫描机制、条件构造器在单数据源下很好用但一旦涉及多数据源它对“每个数据源都要有独立的SqlSessionFactory”这件事是有要求的。而Druid作为连接池胜在监控能力和参数可控性特别是数据源多的时候每个连接池的活跃连接数、慢SQL、瓶颈一目了然。动态数据源的核心还是AbstractRoutingDataSource它在Spring抽象层面解决“从多个DataSource中按Key选择一个”的问题。只不过自己手写这套的话还要处理切面、注解、线程上下文封装成本不低所以成熟项目里一般直接用封装好的starter至少代码层面省心很多。1.3 方案对比和架构取舍如果你还在纠结到底用哪种方案我给你一个参考维度先看你们团队对“黑盒”的容忍度。用starter方案基本就是配置加注解开箱即用出问题也好查它本身有日志和拦截器。用手写方案灵活性和可控性更高但后续维护的人必须非常清楚路由逻辑是怎么实现的。我当时选型的逻辑很简单项目已经用了Mybatis-Plus和Druid多数据源需求属于“业务库隔离”而非“极端高并发读写分离”那么直接用dynamic-datasource这套最省事。它内部基于AbstractRoutingDataSource实现同时对Mybatis-Plus做了适配还额外支持了DS注解可以精细到方法级指定数据源。相比自己搓一个AOP切面可靠性和代码量都强太多。有一点要注意ShardingSphere-JDBC也支持多数据源但如果你的项目只是单纯多库没有分库分表需求引入它会带来额外的分片引擎概念学习成本和排查成本都上升。杀鸡用牛刀没必要。2. 核心细节解析与前置配置2.1 依赖引入版本与兼容性多数据源配置最怕的就是依赖冲突尤其是Mybatis-Plus和动态数据源之间的版本联动。我的经验是Mybatis-Plus的版本尽量和dynamic-datasource的版本保持一个大版本区间内不要用太老的主力版本去配最新的starter也不要一味追新。这里给一份我在某公司项目里实际用过的依赖组合基于Spring Boot 2.7.xdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.6.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency有一点必须说清楚如果你用了dynamic-datasource-spring-boot-starter它内部会自己去创建数据源你就不需要再手写DataSource的Bean配置了否则会冲突。同理Druid的spring-boot-starter在这里的作用主要是提供连接池参数和监控能力但如果你发现动态数据源和Druid的自动配置“打架”可以只保留动态数据源里的Druid配置项去掉druid-spring-boot-starter单独依赖。2.2 主从库配置文件的编写思路配置文件是多数据源里最直观的部分。我习惯在application.yml里按“主库、从库、业务库”三层结构组织。每个数据源下有哪些配置项直接决定连接池行为。下面是一份典型的配置模板spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://127.0.0.1:3306/master_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 validation-query: SELECT 1 test-while-idle: true time-between-eviction-runs-millis: 60000 slave: url: jdbc:mysql://127.0.0.1:3306/slave_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 3 min-idle: 3 max-active: 10 max-wait: 60000 validation-query: SELECT 1 test-while-idle: true time-between-eviction-runs-millis: 60000primary: master表示默认数据源是master。strict: false表示找不到指定数据源时允许降级到默认数据源如果是true找不到就报错。生产环境我建议设成false开发阶段可以临时开true能更早暴露配置问题。Druid参数这块别光抄要理解每个参数的意义。initial-size是启动时初始化几个连接5个足够max-active是连接池上限20是常规线上配置如果你预估并发还更高可以调到50但需要结合数据库的max_connections来看别把连接池配得比数据库上限还大。validation-query和test-while-idle是防止数据库把空闲连接杀掉生产环境基本必配。2.3 Druid监控与防火墙配置连接池的价值不只是连接复用Druid的监控功能在排查多数据源问题时很关键。你需要在配置里单独加StatViewServlet和WebStatFilter这样就能在浏览器里看到每个数据源的连接创建数、活跃数、SQL执行统计。多数据源环境里这个监控面板比日志更直观能一眼看出SQL到底是打到了哪个库上。spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin123 allow: 127.0.0.1 web-stat-filter: enabled: true url-pattern: /* exclusions: /druid/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico filter: stat: enabled: true slow-sql-millis: 1000 log-slow-sql: true wall: enabled: true config: multi-statement-allow: true几点心得allow这里建议只配内网IP别开放远程访问否则等于把数据库前端暴露出去。slow-sql-millis设为1000毫秒是个比较合理的起点多数据源场景下如果某条SQL走了错误的库往往会出现偶发慢SQL这个参数能帮你快速捞到嫌疑SQL。wall防火墙默认开启如果你项目里用了特殊SQL可能被拦截遇到再说一般不用动。3. 动态路由原理与重头戏实现3.1 AbstractRoutingDataSource到底做了什么想要把多数据源用得明白还是得回到Spring的AbstractRoutingDataSource这个抽象类。它本质上是一个DataSource的门面内部维护了一个MapObject, DataSource每个DataSource对应一个key。每次getConnection的时候它会调用determineCurrentLookupKey()方法拿到当前线程应该使用的key然后从map里取出真正的DataSource来创建连接。Spring本身没有实现“当前线程应该用哪个key”的逻辑它把这个留给开发者。这就给了我们很大的扩展空间你可以用ThreadLocal存key也可以用AOP切面去拦截注解还可以按读/写操作自动决定走主库还是从库。所以你在网上看到的各种“动态数据源”框架核心思路都一样区别只在determineCurrentLookupKey()的实现方式。dynamic-datasource这套starter就是把这一套封装好了它在determineCurrentLookupKey()里优先读取DS注解指定的数据源key如果没有注解就返回primary指定的默认key。整个路由链路是请求进来 → AOP拦截到DS注解 → 把数据源key存入ThreadLocal → 调用Mapper时通过AbstractRoutingDataSource取连接 → 用完清理ThreadLocal。3.2 核心注解DS的正确打开方式DS注解可以加在Service方法上也可以加在Mapper接口上。我的建议是加在Service实现类的方法上因为事务边界通常在Service层你在这里切换数据源后续Mapper调用才能吃到正确的数据源。如果直接加在Mapper上其实也能生效但万一你的Service方法里同时操作了多个库事务就只能各管各的非常容易出问题。常见的写法是Service public class OrderServiceImpl implements OrderService { DS(master) public void createOrder(Order order) { orderMapper.insert(order); } DS(slave) public ListOrder queryOrderList() { return orderMapper.selectList(null); } }这里有个值得注意的点如果你在一个Service方法里先调用了DS(slave)的另一个Service方法再回到当前方法操作master那么数据源的切换是以方法调用栈里的最后一次设置为准的。直白点说就是内部方法如果覆盖了数据源key外部方法后续的SQL路由也会受影响。遇到这种情况要么避免在Service之间交叉调用要么内部方法调完把key还原。3.3 手写一个轻量动态数据源路由Demo如果你不想依赖starter想自己控制路由逻辑那理解手写路由是很值得的。我用过手写方案后来因为项目成员结构原因换成了starter但这个Demo依然值得保留在知识库里因为排查问题时会用到这些底层理解。第一步继承AbstractRoutingDataSourcepublic class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSourceKey(String key) { CONTEXT_HOLDER.set(key); } public static String getDataSourceKey() { return CONTEXT_HOLDER.get(); } public static void clear() { CONTEXT_HOLDER.remove(); } Override protected Object determineCurrentLookupKey() { return getDataSourceKey(); } }第二步用AOP切面拦截注解Aspect Component public class DataSourceAspect { Around(annotation(ds)) public Object switchDataSource(ProceedingJoinPoint joinPoint, DS ds) throws Throwable { try { DynamicDataSource.setDataSourceKey(ds.value()); return joinPoint.proceed(); } finally { DynamicDataSource.clear(); } } }第三步配置多个DataSource并注入到DynamicDataSource中Configuration public class DataSourceConfig { Bean public DataSource masterDataSource() { // 构建DruidDataSource } Bean public DataSource slaveDataSource() { // 构建DruidDataSource } Bean public DataSource dynamicDataSource() { DynamicDataSource dataSource new DynamicDataSource(); MapObject, Object targetDataSources new HashMap(); targetDataSources.put(master, masterDataSource()); targetDataSources.put(slave, slaveDataSource()); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(masterDataSource()); return dataSource; } }手写方案的最大问题在于SqlSessionFactory和事务管理器都得手动绑定到这个动态数据源上否则Mapper走的数据源和事务控制的数据源对不上。一旦你决定手写就得把DataSource、SqlSessionFactory、TransactionManager这三者的创建全部接管。我当时的体会是手写能让你把原理吃得透透的但生产环境还是用封装好的框架更高效尤其团队人多的时候框架约定比个人习惯可靠得多。3.4 基于Mybatis-Plus的SqlSessionFactory配置不管用不用starter你都绕不开一个关键点Mybatis-Plus的SqlSessionFactory如果只绑定了一个数据源那Mapper只会操作那一个库。所以当使用手写动态数据源时一定要把dynamicDataSource注入到MybatisPlusSqlSessionFactory的构造参数里。Bean public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dynamicDataSource) throws Exception { MybatisSqlSessionFactoryBean factoryBean new MybatisSqlSessionFactoryBean(); factoryBean.setDataSource(dynamicDataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath*:mapper/**/*.xml)); MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); interceptor.addInnerInterceptor(pagination); factoryBean.setPlugins(interceptor); return factoryBean.getObject(); }用Mybatis-Plus时MybatisSqlSessionFactoryBean这个类是必须的不能直接用原生的SqlSessionFactoryBean否则Mybatis-Plus的自动填充、分页插件、逻辑删除全都失效。如果你用的是dynamic-datasource starter它会自动处理SqlSessionFactory绑定多数据源的事这也是我推荐它的核心原因之一省心且不会漏配。4. 实操过程从零搭建一套可运行的多数据源4.1 完整项目结构规划开始写代码之前先规划好包结构和数据源命名。我见过不少项目数据源命名混乱master、primary、main混着用最后切数据源时都不知道该写哪个。推荐统一命名master、slave、bizdb这种语义清晰且简短。项目结构大致是这样的src/main/java ├── com.example.demo │ ├── config │ │ └── MybatisPlusConfig.java │ ├── controller │ │ └── UserController.java │ ├── datasource │ │ └── DynamicDataSource.java │ ├── mapper │ │ └── UserMapper.java │ ├── model │ │ └── User.java │ └── service │ ├── UserService.java │ └── impl │ └── UserServiceImpl.java src/main/resources ├── mapper │ └── UserMapper.xml └── application.yml模块划分清晰有个直接好处排查问题时能快速定位是多数据源配置的问题还是业务SQL的问题还是Mapper映射的问题。实际开发中这三类问题经常混在一起如果耦合在一块debug成本很高。4.2 从零手把手配置一个读写分离Demo我以“主库负责写入、从库负责查询”的经典读写分离场景为例带你走一遍完整搭建过程。第一步在application.yml里定义两套数据源。这一步上面已经给过模板就不再重复。唯一提醒的是driver-class-name务必用com.mysql.cj.jdbc.Driver这是MySQL 8 的驱动类名旧版的com.mysql.jdbc.Driver在新版本里会报警告。第二步配置Mybatis-Plus的常用插件。读写分离场景下分页是高频功能所以分页拦截器必配Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }这里setMaxLimit(500L)是我比较推荐的设置防止误操作查出超大数据集把从库压垮。多数据源环境下从库往往承担着大量查询流量一旦有人写了个不带条件的全表分页很容易拖垮整个库。第三步定义实体和MapperData TableName(user) public class User { TableId(type IdType.AUTO) private Long id; private String name; private Integer age; }Mapper public interface UserMapper extends BaseMapperUser { }第四步Service层用DS切换数据源Service public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { DS(master) public void addUser(User user) { save(user); } DS(slave) public ListUser listUsers() { return list(); } }这样addUser会往主库写listUsers会从从库读。我建议在Controller里加一个测试接口分别调用两个方法然后在两个库里造不同的数据比如主库存了一条“master-user”从库存了一条“slave-user”分别访问看返回的数据就能验证路由是否生效。4.3 多业务库场景的按需路由读写分离是“同一份业务数据不同职责的库”多业务库则是“不同业务模块完全独立的库”。比如用户表在user库订单表在order库。这种情况下基本思路是每个业务模块的Service方法上明确指定数据源不依赖默认数据源。Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private UserMapper userMapper; DS(order) public void createOrder(Order order) { orderMapper.insert(order); } DS(user) public User getUserById(Long userId) { return userMapper.selectById(userId); } }这样的好处是职责一目了然坏处是如果一个Service方法里需要同时操作多个库就会面临事务无法跨库的问题。这一点我在下一节会专门讲这也是多数据源配置里真正容易让人崩溃的地方。4.4 参数调优和监控验证配置完成后除了功能验证还建议做一次参数层面的验证。重点看两件事连接池是否按预期初始化、Druid监控能否区分出不同数据源的连接数据。连接池参数我一般用下面的基线具体数值按业务压力调整参数master值slave值说明initial-size53启动时创建连接数min-idle53最小空闲连接数max-active2010最大活跃连接数max-wait6000060000获取连接最大等待时间validation-querySELECT 1SELECT 1连接检查SQLtime-between-eviction-runs-millis6000060000空闲连接检测间隔生产环境里max-active要根据压测结果调整。很多团队都有过max-active配太大把数据库拖垮的经历也有配太小导致高峰期连接不够报错的。我的经验是先按容量预估50%压测后再慢慢上调不要一下给满。监控验证这块Druid面板上如果看到两个数据源的URL分别出现且SQL执行数在增长说明多数据源配置已经生效。如果你发现所有SQL都打在同一个数据源上优先检查DS注解有没有被AOP正确拦截到。5. 常见问题与排查技巧实录5.1 最典型的几个报错现象多数据源配置最常见的报错我整理成了一张表每一类我都碰到过至少一次现象根本原因解决方向启动报DataSourceUrl未设置dynamic-datasource的配置被Druid自动配置覆盖调整依赖或排除Druid自动配置运行时发现SQL走错库DS注解加的位置不对或AOP没生效把注解加到Service实现方法上并检查切面扫描Mapper查询报找不到表Mybatis-Plus的SqlSessionFactory绑定了单个数据源确保SqlSessionFactory绑定动态数据源事务方法内切换数据源失效事务开启时连接已经绑定DS无法改变当前事务连接避免事务方法内切换或使用支持XA的事务方案提示Cannot determine target DataSourcestrict: true且找不到注解指定的数据源检查数据源名称拼写或临时改strict为false第一类问题非常典型。用了druid-spring-boot-starter再配dynamic-datasource有时启动会报url not set。这是因为Druid的自动配置尝试创建一个默认数据源却没有找到spring.datasource.url。解决办法是排除Druid的自动配置类SpringBootApplication(exclude DruidDataSourceAutoConfigure.class) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }老实说踩过这个坑之后我才意识到动态数据源和Druid自动配置在某些版本里兼容性并不完美“能用”和“顺利”还是有区别的遇到这种问题不要怀疑自己配错了参数先排除自动配置冲突。5.2 事务与多数据源的世纪难题这是我特别想展开聊的一点。Spring的Transactional默认只能控制一个数据源的事务如果你在一个事务方法里先操作了master又切到slave查询那么slave的查询是不会参与当前事务的。更麻烦的是事务开启时连接已经绑定到master此时DS注解即使指定了slave也可能不会生效因为连接持有者还是master。网上有很多人说把Transactional加在多数据源方法上就能搞定这是不对的。实际生产环境里真正能保证跨库一致性的方案只有几种分布式事务中间件比如Seata、本地消息表最终一致性、或者干脆拆分服务每个服务管自己的库。我的建议很直接如果只是读操作需要走从库就别加Transactional如果是写操作且必须保证一致性就用独立的事务边界并把多个库的写入拆分到不同的方法里配合业务补偿。不要试图在单个事务方法里既跨库又保证强一致那基本是给自己挖坑。5.3 动态数据源配置踩坑避坑要诀除了上面说的我另外总结几条独家心得一是DS注解和Transactional的优先级关系。当两个注解同时存在时Spring事务的拦截器会先于数据源切面执行所以连接池可能先拿到了默认数据源的连接。即使你后来切了数据源事务管理的连接还是旧的。这个顺序问题很隐蔽排查时容易绕圈子。二是一个数据源对应一套Mybatis-Plus配置。如果你有几个库且每个库的数据库方言或分页规则不一致需要检查分页插件是否支持多方言识别。dynamic-datasource在大多数场景下能处理但如果你用手写方案分页插件只能绑定一个方言MySQL和Oracle并存时会出问题。三是连接池泄漏问题。多数据源环境下如果少了一个关闭连接的逻辑连接池会被耗尽表现就是应用卡死、报连接超时。排查这类问题建议开启Druid的remove-abandoned配置druid: remove-abandoned: true remove-abandoned-timeout: 180 log-abandoned: true这条配置能帮你自动回收泄漏的连接同时把泄漏点日志打印出来定位代码位置很方便。生产环境建议开一阵子等稳定了再视情况关掉。5.4 写操作走主库、读操作走从库的细节优化读写分离场景下如果你不想在每个读方法上都加DS可以考虑用一个切面做自动路由。判断逻辑可以很简单方法名以get/select/query/list/count开头的走slave其余走master。这个方案很粗暴但确实能减少注解遗漏。不过我不太推荐在重要项目里全量依赖方法名约定毕竟总有人不按规矩起名字。更稳妥的做法是默认数据源保持master只有明确的查询方法才加DS(slave)。这样即使你漏加了注解SQL也是打到主库不会出现查询走到了从库但数据同步存在延迟导致读到旧数据的问题。毕竟主从延迟是读写分离绕不开的坑读从库意味着默认接受一定的数据延迟。一点个人感触读写分离的真正难点不在配置而在于业务是否允许短暂的读延迟。如果你的业务强一致要求很高比如刚写完订单立刻要查到订单那读写分离天然不适用这种情况必须强制走主库或者绕过从库查询逻辑。5.5 注解失效与切面优先级问题DS注解失效是另一个高频问题排查方向主要有三个。第一个是AOP切面没有被Spring管理注意Aspect和Component这两个注解都要有少了任何一个切面都不起作用。第二个是自调用问题即同类内部调用不走代理Spring AOP天然无法拦截。第三个是切面顺序问题数据源切面一定要保证在事务切面之前生效可以通过Order(Ordered.HIGHEST_PRECEDENCE)来提升数据源切面的优先级。Aspect Component Order(Ordered.HIGHEST_PRECEDENCE) public class DataSourceAspect { // ... }如果你用的是dynamic-datasource starter它内部已经处理了大部分切面逻辑遇到失效概率会小很多。但还是建议你把切面知识留着因为这个坑一旦出现你排查起来会很有方向感。6. 结语与实操体会用了这么久的多数据源方案我个人最深的体会是配置本身只占20%剩下80%的精力都花在“理解路由时机、梳理事务边界、排查隐性问题”上。无论是Mybatis-Plus的SqlSessionFactory绑定还是Druid连接池的参数调优又或者是DS注解和事务的相互作用每一个点背后都有它在Spring生态里的设计逻辑。你越懂底层配置报错时就越不容易慌。最后再分享一个我个人的小技巧多数据源配置完成后不要急着写业务代码先做一个“数据源连通性验证”的小接口把每个数据源都查一遍再在Druid监控面板上核对两套连接池的URL。这一步看似多余实际上能把90%的配置错误拦在业务开发之前省下后面一大段debug时间。项目里的其他成员接手多数据源相关需求时我也不会直接给文档而是带着他们把这个验证流程走一遍比讲十遍配置原理都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑