资讯动态

SpringBoot+MyBatis-Plus多数据源实战:从配置到踩坑

发布时间:2026/10/9 12:45:01 来源:尧图企业网站定制
如果你跟我一样在业务增长比较快的团队里做后端大概率早晚会遇到同一个SpringBoot项目连两个甚至多个数据库的需求。我最早碰多数据源是在订单和报表分库的时候主库扛写入报表库跑复杂查询两边数据要在一个服务里同时访问。当时第一反应是“直接配两个DataSource不就行了”实际动手才发现切换逻辑、事务边界、连接池管理、Mapper归属全是细节活。后来换到mybatis-plus生态之后配置多数据源这件事变得非常省心基本可以做到开箱即用。这篇把从选型到落地的完整过程写清楚包括依赖版本、YAML配置、DS注解的使用姿势以及我实际运行中踩过的几个坑。只要你的项目用的是SpringBoot mybatis-plus这套可以直接抄。1. 为什么要连多个库多数据源不是炫技而是刚需先说需求场景避免一上来就配代码的人搞不清自己在解决什么问题。多数据源在真实业务里基本逃不开下面几类情况。1.1 读写分离最经济实惠的“加一台读库”大部分系统的瓶颈都在数据库查询尤其是报表、列表、统计这类读多写少的场景。把只读流量切到从库主库专心处理写入是成本最低的扩容方式。读写分离落到代码层面就是写操作走主库查询操作走从库同一个Service里根据方法自动切换数据源。mybatis-plus官方生态里做这件事的核心工具是dynamic-datasource-spring-boot-starter它提供的DS注解可以直接挂在Service方法上方法执行时自动路由到指定数据源不需要手写切库逻辑。我用下来的体感是配置一次后面基本忘掉它的存在。1.2 业务域拆分一个服务面对多个业务库另一个高频场景是业务库拆分。用户库、订单库、商品库不放在同一个MySQL实例里物理隔离互不影响。这种架构在微服务化之前很常见一个单体服务先连着多个库跑等业务边界清晰了再慢慢拆成独立服务。多数据源方案正好给了这个过渡期一个低成本的技术支撑不至于因为物理分库就把服务拆得七零八落。1.3 多租户和跨库汇总让维度再增加一层SaaS系统经常按租户分库租户A的数据在tenant_1租户B在tenant_2登录之后从上下文里取租户ID动态路由。这种动态数据源用DS配合SpEL表达式也能做不需要在代码里写死。还有一种跨库汇总需求定时任务凌晨从十几个分库里捞数据聚合写入统计库。这个时候数据源的数量是运行期动态变化的配置文件的静态写法就不够用了需要考虑运行期注册数据源。dynamic-datasource也支持这种扩展不过日常项目中90%的需求用静态配置加DS就能覆盖。2. 多数据源方案横评自己写路由、硬编码SqlSessionFactory与dynamic-datasource大多数人在做多数据源时第一反应不是引入新组件而是想“这么简单的事情自己写一下”。我全都试过说点真实感受。2.1 自己写AbstractRoutingDataSourceThreadLocal看似简单坑在细节Spring本身提供了AbstractRoutingDataSource配合AOP切面和ThreadLocal确实能实现动态切换。核心逻辑不复杂定义一个DynamicDataSourceContextHolder用ThreadLocal存当前数据源key在切面里设置key执行完清理掉。这套方案的问题不在“能不能跑通”而在细节。第一个坑是ThreadLocal清理如果异常路径上没有正确执行remove()线程池复用线程时会把上一个请求的数据源带出来导致下一个用户莫名其妙查到别的库。排查这种问题非常痛苦它不报错只是数据不对。第二个坑是嵌套切换一个方法切到从库内部又调用另一个需要切到主库的方法如果AOP顺序控制不好数据源就会乱。自己写方案表面省了依赖实际上把大量边界问题揽到了自己身上。2.2 每库一套SqlSessionFactory配置膨胀到无法维护还有人用最原始的方式每个数据源单独配一套DataSource、SqlSessionFactory、MapperScannerConfigurerMapper接口按库分包管理比如com.example.mapper.order和com.example.mapper.user。这套方案胜在直观每个库都是独立的一套MyBatis配置互不干扰。缺点是配置量爆炸每加一个库要复制一大段重复的Java配置类Mapper分包带来包名维护成本跨库联查别想连事务都各自独立。团队里如果有多个人同时开发这个方案很快会变成配置地狱。2.3 选择dynamic-datasource的理由官方维护、生态成熟、注解式低侵入后来我把方案统一到了dynamic-datasource-spring-boot-starter。这个组件是MyBatis-Plus官方生态的一员社区活跃度很高文档齐全迭代也快。它对多数据源的封装做得比较彻底维度自己写路由多套SqlSessionFactorydynamic-datasource接入成本需要自己写切面和上下文每库一套配置类依赖加配置即可Mapper层改动无按库分包管理无Mapper接口随意放数据源切换方式AOP切面手动编码无切换各用各的DS注解方法级控制嵌套切换容易乱天然不支持支持内部做了栈式管理事务支持需要自己处理各库独立事务DSTransactional 分布式事务方案运维成本ThreadLocal泄漏风险配置冗余官方持续修复选了它之后就进入正题怎么接进来怎么配置怎么把坑避开。3. 开箱即用的三步接入依赖、YAML配置与DS注解接入过程确实是三步加依赖、写配置、加注解。但每一步都有细节尤其是版本。3.1 版本不匹配是第一个拦路虎Spring Boot版本太高引发的连锁反应先说最容易被绊倒的地方。dynamic-datasource-spring-boot-starter的版本和SpringBoot版本强相关。Spring Boot 2.x用的是旧版SpringSpring Boot 3.x不仅包名从javax.*换成了jakarta.*自动配置的加载机制也从spring.factories改成了AutoConfiguration.imports。如果你用Spring Boot 3.x还引入3.3.x的旧版dynamic-datasource它内部的自动配置类根本不会生效数据源压根不初始化或者直接报DataSource找不到之类的错误。我的建议是引入之前先对着官方GitHub文档确认兼容矩阵。大致对应关系是这样的Spring Boot版本dynamic-datasource建议版本1.5.x2.5.7及以下2.3.x3.3.x2.5.x ~ 2.7.x3.4.x / 3.5.x3.x4.x或官方最新版实际用的时候直接以Maven Central上的最新发布为准别迷信网上老教程里的版本号。我见过最典型的问题是Spring Boot 2.7的项目配了3.6.0的dynamic-datasource功能没问题但Spring Boot 3.2的项目用3.5.2启动直接报错。所以先说结论先确认版本兼容再写配置这一步能省掉后面80%的排查时间。3.2 基础YAML配置主从两个库的官方推荐写法依赖就一个starter没有其他多余的东西dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version${dynamic-datasource.version}/version /dependency如果项目中原本有spring-boot-starter-jdbc或MyBatis相关依赖不需要做额外排除starter会自动接管数据源装配。YAML配置遵循spring.datasource.dynamic前缀。下面是一个最典型的主从配置spring: datasource: dynamic: primary: master strict: false datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/master_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 slave_1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/slave_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 hikari: max-pool-size: 20 min-idle: 5 connection-timeout: 30000几个配置项要理解清楚primary默认数据源不加DS注解的方法都走这里。大多数项目直接把主库设成primary。strict是否开启严格模式。false时如果代码里指定了一个不存在的数据源key不会报错而是静默降级到primarytrue时会直接抛异常。生产环境我建议设成true防止手滑写错名字导致数据写到主库。datasource下的每个节点就是具体的数据源key是自定义的DS注解里写的就是这个key。公共连接池配置写在hikari或druid节点下所有数据源共享。组件默认使用HikariCP想切到Druid就在依赖里加Druid然后在每个数据源节点或公共节点指定type。多数据源最忌讳“每个库配一套连接池参数”连接数忽高忽低所以能用公共配置就统一用公共配置。3.3 DS注解的用法细节作用位置、覆盖优先级和SpEL动态解析DS是整套方案的核心API。它可以用在类上也可以用在方法上还可以用在Mapper接口上。优先级是方法 类 全局primary。也就是说方法上有DS就听方法的方法没有就听类上的类也没有就走primary。我在实际项目中的习惯是Service实现类上不写DS默认主库特殊情况在具体方法上标注。比如下面的代码Service public class OrderServiceImpl extends ServiceImplOrderMapper, Order implements OrderService { Override public void createOrder(Order order) { // 该方法默认走primary也就是主库 save(order); } Override DS(slave_1) public ListOrder pageOrders(PageOrder page) { // 查询走slave_1只读库 return page(page, new LambdaQueryWrapper()).getRecords(); } }注意DS加在方法上时整个方法体内部的所有数据库操作都路由到指定数据源。如果方法内部又调用了另一个带有DS的bean方法会发生嵌套切换组件内部用栈结构管理调用链切换结束后会恢复到上一级数据源。DS还支持SpEL表达式实现运行期动态路由。比如多租户场景DS(#tenantId) public ListUser listUser(String tenantId) { // 根据tenantId动态选择数据库 }参数名tenantId会作为SpEL变量参与解析框架从当前线程上下文里取值切换数据源。这种动态写法让我在租户隔离场景省了很多重复代码。4. 稳定运行后遇到的坑代理失效、事务纠缠和吞掉异常的路由接入阶段很顺利但真正上线后我陆续遇到了几个比较棘手的问题每一个都值得单独说说。4.1 同类内部调用导致DS失效这是使用Spring注解最经典的坑DS也一样。Spring处理DS靠的是AOP代理通过代理对象调用方法时注解才会生效。如果一个类的a方法直接调用了同类里的b方法b上的DS不会生效因为内部调用走的是this不是代理对象。我第一次遇到时现象很诡异两个方法都在同一个Service类里查询方法明明标了DS(slave_1)日志里却显示走了主库。排查了半天发现是这类内部调用导致的。有人会加自依赖解决比如Service public class UserServiceImpl { Autowired private UserServiceImpl self; public void outerMethod() { self.readOnlyMethod(); } DS(slave_1) public void readOnlyMethod() { // 走从库查询 } }这样绕开了this调用注解能生效。但这个写法不太优雅容易绕晕后面接手的人。更好的方式是拆Service或者把需要切换数据源的方法放到不同类里从设计上规避内部代理失效。我在项目里的做法是每个库的读写操作拆成独立的Service方法块不互相嵌套调用。4.2 DS与Transactional“打架”时的两种典型现场DS和Transactional同时出现时先后顺序很关键。事务一旦开启数据源连接就被绑定到了当前事务上。如果先开启事务再切换数据源切换动作很可能不生效甚至因为连接归属不同导致拿到两个连接事务的一致性无从谈起。dynamic-datasource对这种情况的处理是优先让事务感知数据源切换但本质上本地事务只能绑定一个数据源。我遇到过的典型现场有两种一种是Service方法上同时标注了DS(slave_1)和Transactional整个方法的事务建立在从库连接上里面有写操作时数据写到了从库而不是主库。这在主从结构下是致命问题从库的数据会被覆盖。另一种是切到从库执行查询但方法内调用了外部写入接口结果写入也落在了从库从库复制链路直接异常。我的建议是只读方法不要加Transactional写操作明确只走主库必须跨库做事务时不要再想用本地事务解决。官方提供的DSTransactional可以在一定程度上聚合多个数据源的本地事务但它不是真正的分布式事务适合对一致性要求不苛刻的场景。真正需要严格跨库一致性时要借Seata这类分布式事务框架。4.3 strict模式与找不到数据源的静默降级前面说过strict: false时如果DS里写的key在配置中不存在会静默走primary。这个行为在开发阶段问题不大但在生产环境是隐患。团队里如果有人把DS(slave_1)错写成DS(slave1)代码不会报错只是所有查询流量悄悄压到了主库等主库CPU飙升才发现。我上线后就把所有项目的strict都改成了true。这样配置错误会在调用时立刻抛出异常问题暴露在测试阶段而不是线上。虽然多了一行提示信息但能避免一个隐蔽的线上故障。4.4 多数据源的事务问题到此为止了吗边界问题不止是注解顺序还有更复杂的跨库事务。一个业务操作需要同时更新主库和订单库时本地事务无能为力。说句实在话数据源切换本身不难难的是事务一致性设计。我在项目里的处理原则很简单一个业务方法只操作一个库跨库的数据变更通过异步消息或定时任务补偿。如果业务真的强依赖跨库事务就老老实实引入分布式事务组件不要幻想注解能解决。对大多数中小项目来说把跨库操作拆成单库操作加最终一致远比上一套分布式事务框架省心。4.5 跨数据源查询的思路多数据源环境下写SQL还有个问题不能直接跨库join。不同物理库之间MySQL的FEDERATED引擎用起来性能堪忧应用层做内存关联又内存开销大。常规做法是把数据聚合放到应用层或者引入ES、ClickHouse这种列式存储做跨库汇总。如果只是报表统计用定时任务把多库数据洗到统计库是最稳妥的方式。5. 完整示例订单模块读写分离怎么落地理论讲再多不如一个完整demo看着直观。下面用一个订单模块演示主从读写分离的落地方式。5.1 项目依赖与结构除了SpringBoot基础依赖外核心依赖主要有两个dynamic-datasource starter和mybatis-plus starter。版本上注意和SpringBoot匹配。dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.6.0/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency项目结构保持正常的分层方式Controller、Service、Mapper不需要额外分包。5.2 Service层注解与Mapper层注意事项订单写入和查询分别落到不同库代码很直白public interface OrderService { Long createOrder(Order order); ListOrder queryPage(PageOrder page); } Service public class OrderServiceImpl implements OrderService { Resource private OrderMapper orderMapper; Override public Long createOrder(Order order) { orderMapper.insert(order); return order.getId(); } Override DS(slave_1) public ListOrder queryPage(PageOrder page) { return orderMapper.selectPage(page, new LambdaQueryWrapper()).getRecords(); } }注意几个细节写入方法没加DS走的是primary主库。查询方法标了DS(slave_1)只走从库。Mapper接口不需要区分主从不用分包。如果哪天要从库里加一个数据字段直接改Mapper接口不用动DataSource任何配置。这就是注解式方案比多套SqlSessionFactory舒服的地方。5.3 用日志和SQL打印验证数据源切换是否真的生效写完之后第一件事不是上线而是验证切换是否生效。我推荐一个简单办法打开MyBatis的SQL日志看打印的SQL里有没有带上数据源信息。在application.yml里配置日志级别logging: level: com.example: debugMyBatis-Plus本身打印的SQL不带数据源标识想更直观的话可以临时在每个数据源配置里加一个spring.datasource.dynamic.datasource.*的连接初始SQL比如datasource: master: connection-init-sql: SELECT master_db slave_1: connection-init-sql: SELECT slave_db也可以直接在业务代码里临时注入DynamicDataSourceContextHolder打印当前数据源keyString current DynamicDataSourceContextHolder.peek(); log.info(current datasource: {}, current);这样一眼就能看出查询方法实际路由到了哪个库。验证无误之后把调试代码删掉。6. 多数据源与周边组件的配合细节最后一个部分聊聊多数据源和项目里其他常用组件的配合。这些东西不会写进官方demo里但生产环境一定会碰到。6.1 和定时任务、MyBatis-Plus插件的共存项目里如果用了Spring的Scheduled定时任务定时方法上同样可以加DS注解切换数据源。因为定时任务的方法也是通过代理对象执行的只要注解标注正确路由会生效。但注意定时任务里千万不要开长事务多数据源环境下长事务把连接占住很容易把连接池拖垮。MyBatis-Plus的分页插件、乐观锁插件、逻辑删除这些组件在多数据源下不需要额外配置它们作用于SqlSessionFactory层面和哪个数据源无关。唯一需要注意的是插件在执行过程中会做SQL改写如果你的某个数据源是Oracle或PG这种非MySQL数据库分页方言会用错地方。这种情况需要针对不同数据库类型配置多个分页方言拦截器按数据源区分处理。6.2 生产环境的数据源安全与监控多数据源意味着多个数据库账号密码出现在配置里密码明文写在application.yml里太危险。我这边做法是用Jasypt对密码加密或统一走Nacos、Apollo配置中心密码从配置中心动态拉取。启动时动态数据源支持通过环境变量覆盖实际部署时可以把密码配置到环境变量里。监控层面如果有Druid连接池可以开Druid的监控页或者接入Prometheus指标。多数据源环境下连接池数量变多每个数据源的活跃连接数、等待线程数都要盯住避免某一个库连接耗尽拖垮整个服务。6.3 几个让整个方案更健康的习惯最后说几个偏“习惯”层面的建议。多数据源配置保持精简别把十几个库全塞进YAML里配到最后没人分得清哪个key对应哪个库。数据源命名用业务语义比如order_db、user_db别用ds1、ds2这种无意义名字。每次新增数据源或修改DS注解都要在测试环境验证一下路由结果这个成本远低于在线上踩雷后再去排查。遇到路由不生效的问题优先检查是不是代理失效或SQL打印显示走了默认库排查顺序是版本兼容性、注解位置、同类内部调用、事务绑定。按这个顺序走基本十分钟内能定位问题。

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

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

免费获取报价 →
↑