
SpringBoot【十一】mybatis-plus实现多数据源配置开箱即用多数据源这个需求说大不大说小不小。我接手过的项目里十有八九遇到过这种局面业务库拆了两个读写分离要落地或者报表系统得从独立的库里拉数据。SpringBoot MyBatis-Plus 的组合自然是熟得不能再熟可一说到多数据源很多人第一反应就是自己写个 AOP 切面搞个AbstractRoutingDataSource再整个ThreadLocal存当前库的 key一套方案撸下来确实能用但维护成本一点不少代码里那股手动切库的味道也挥之不去。这篇文章要说的是我自己在项目里实测下来最省事的方案用 MyBatis-Plus 官方配套的dynamic-datasource-spring-boot-starter配置两段 yml写一个DS注解就完成多数据源接入。开箱即用这四个字真不是宣传话术我踩完坑回头再看这玩意确实是目前 SpringBoot 生态里最顺手的多数据源轮子。这篇文章我会把完整的配置步骤、切换原理、事务坑位和几个项目经验里的排查技巧都交代清楚适合正在玩 SpringBoot想让项目一次性搞定多数据源切库的 Java 开发。1. 为什么需要多数据源先搞清楚出发点和方案选型多数据源不是炫技是业务被逼出来的。你得先想明白自己为什么需要多数据源再动手搞不然大概率会把代码越搞越乱。1.1 多数据源的常见落地场景我遇到过的场景大致分三类你对照着自己项目看看属于哪种第一类是读写分离。主库扛写入从库分担查询压力这是最经典的多数据源诉求。好处是主库不被慢查询拖死从库可以挂多个分摊读流量查询接口响应速度也能提上来。这种场景下你通常希望select走从库insert/update/delete走主库。第二类是业务库隔离。微服务架构下一个服务可能需要访问多个不同的业务库比如订单服务既要查自己的订单库又要调用户中心的用户信息库。两个库可能在不同的数据库实例上甚至不同的机器上。这种场景下你不能把两个库的数据表放到同一个数据源里统一访问必须按库维度切分。第三类是报表与分析库。业务数据和报表数据常常分开存线上交易库要保持轻量报表库专门跑聚合查询。那你跑报表任务时就得切换到报表库去干活。这三种场景核心诉求都是一个同一套代码里按不同业务逻辑连接到不同数据库执行 SQL。1.2 三种主流实现方案对比既然确定了要搞多数据源那方案选型就得认真做一下因为这个决定直接影响后面几周甚至几个月的开发效率。第一种手动切库。自己在代码里写个DataSourceContextHolder用一个ThreadLocal存当前线程要用的数据源 key然后在 Mapper 调用前手动 set 一下用完再 clear。代码侵入性强每个要切换的地方都得写一遍切换逻辑漏了 clear 还会导致线程池复用时的数据源串线问题。这个方案我早期写过一次后来再也没用太容易出线上事故。第二种AOP AbstractRoutingDataSource。Spring 自带的AbstractRoutingDataSource允许你通过determineCurrentLookupKey()方法动态决定当前用哪个 DataSource。配合 AOP 切面在方法执行前把目标数据源 key 放进去执行后清掉。这个方案比手动切库优雅但依然要自己维护切面逻辑、注解定义、异常处理工程化成本不低。第三种MyBatis-Plus 官方的 dynamic-datasource-spring-boot-starter。这个组件本质上也是基于AbstractRoutingDataSource实现动态路由但它的价值在于把切面逻辑、注解体系、数据源初始化、连接池管理全部封装好了。你只需要配一个 yml写一个DS注解剩下的一切框架自动处理。这三种方案的对比我整理了一个表格对比维度手动切库AOP AbstractRoutingDataSourcedynamic-datasource starter配置复杂度高全部手写中切面和注解自己写低两段 yml 注解代码侵入性高每个切换点手写逻辑中注解仍需自己定义低直接用 DS事务支持不完善需自己处理内置本地事务增强数据源监控无无支持 Druid 监控、日志打印社区维护无无活跃随 MP 版本更新坑位数量极多中等较少文档齐全体验过这套 starter 后我个人的结论很明确除非你的多数据源逻辑极端复杂必须完全自定义路由策略否则优先用 MP 的 dynamic-datasource 就对了。不是为了省事才选它而是它的封装质量确实比你自己动手要强得多。2. 快速接入三步完成多数据源配置这一节直接进入实操。我用的环境是 Spring Boot 2.7.x MyBatis-Plus 3.5.xJDK 1.8。如果你用的是 Spring Boot 3.x记得把 dynamic-datasource 的依赖换成dynamic-datasource-spring-boot3-starter整体包名和用法基本一致。2.1 引入依赖与版本说明在pom.xml里加上dynamic-datasource-spring-boot-starter这个依赖dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version4.3.1/version /dependency同时确保你已经有mybatis-plus-boot-starter依赖。版本上我建议mybatis-plus用 3.5.x 系列比如 3.5.3.1 或更高。dynamic-datasource 4.x 对 MyBatis-Plus 3.5.x 匹配得很稳定我实测下来没有遇到冲突。一个小提示这个 starter 并不会把 MyBatis-Plus 再拉一份进来它是独立的数据源路由组件和 MP 是配套关系。你原来的 MyBatis-Plus 配置、分页插件、SQL 日志插件都还能继续用不需要改动。2.2 多数据源核心配置两段 yml 搞定接下来是application.yml里的配置。先看一个最典型的双数据源配置spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://127.0.0.1:3306/business_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://127.0.0.1:3306/readonly_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver注意几个关键参数spring.datasource.dynamic.primary指定默认数据源的 key这里是master。当代码里没有用DS明确指定数据源时框架自动走这个 key 对应的数据源。spring.datasource.dynamic.strict严格模式开关。false表示非严格模式如果某个 Mapper 或方法指定了不存在的数据源 key不会报错而是回退到默认数据源。true则表示找不到目标数据源直接抛异常。我建议开发阶段开true能快速暴露配置错误线上可以关掉防止某个数据源挂了影响主流程但这么干自己要权衡。每组datasource.xxx就是一个数据源key 名随意但不要重名也不能用primary作为 key。primary是保留配置项。加密的密码、连接池参数、Druid 监控这些都可以配后面我会单独讲连接池调优的细节。但基本的双数据源配置到这里就已经可以跑起来了。2.3 启动类排除默认数据源为什么要排除这一步很多第一次用的人容易漏。在 Spring Boot 启动类上加一个排除配置SpringBootApplication MapperScan(com.example.mapper) // 关键排除 DataSourceAutoConfiguration SpringBootApplication(exclude { org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.class }) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }为什么要排除原因在于Spring Boot 默认会根据spring.datasource.url自动装配一个数据源。而我们多数据源场景下配置的是spring.datasource.dynamic.datasource这个嵌套结构没有顶层的spring.datasource.url不排除默认自动装配的话Spring Boot 会尝试按照单数据源的逻辑去加载启动容易报Failed to configure a DataSource或者Cannot determine embedded database driver class for NONE之类的错误。排除之后数据源就完全交给 dynamic-datasource 来管理了。这一步做完启动你的 SpringBoot 项目能看到日志里打印出多个数据源的初始化信息说明配置生效了。注意如果你的项目用了spring-boot-starter-data-jdbc或 JPA排除DataSourceAutoConfiguration后还需要额外确认 JPA 的相关配置最好让项目里只保留 MyBatis-Plus 这一套数据访问层避免两套框架争抢数据源。3. 使用 DS 注解实现数据源切换原理与实操配置完成只是第一步真正每天都在写的是DS注解。这个注解的使用粒度、放置位置直接决定了多数据源好不好用。3.1 DS 注解的四种使用方式DS可以放在四个不同的层级上我全部都实测过放在 Service 实现类上整个类里的方法都走指定数据源。Service DS(slave) public class UserQueryServiceImpl implements UserQueryService { Autowired private UserMapper userMapper; Override public ListUser listAll() { return userMapper.selectList(null); } }放在 Service 方法上只对方法生效类上没有DS时老方法走默认数据源新方法走指定数据源。Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Override DS(slave) public ListOrder listOrders() { return orderMapper.selectList(null); } Override DS(master) public void createOrder(Order order) { orderMapper.insert(order); } }放在 Mapper 接口方法上直接控制 Mapper 层面的数据源适合 Mapper 之间明确分库的场景。public interface UserMapper extends BaseMapperUser { DS(slave) ListUser selectAllFromSlave(); DS(master) int insertToMaster(User user); }配合自定义注解使用如果你不喜欢在业务代码里写字符串 key可以自定义一个注解做封装。Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) DS(slave) public interface SlaveDataSource { }这个方法在团队规范比较严格的项目里很好用比如只能允许MasterSource和SlaveSource出现在代码里不允许业务开发直接写DS(db2-222-xxx)这种魔法字符串代码审查的时候清爽很多。3.2 数据源切换原理从 AOP 到 ThreadLocal用了DS之后你可能会好奇它背后是怎么做到自动切换的。这里我用最直白的方式讲清楚整个链路理解了原理后面排坑会容易很多。整个机制的核心是 Spring 的 AOP 与AbstractRoutingDataSource的组合。dynamic-datasource 在启动时注册了一个DynamicDataSourceAnnotationAdvisor切面这个切面专门处理DS注解。当你的业务方法带着DS被调用时切面拦截器会执行一个DynamicDataSourceContextHolder.push(dataSourceKey)操作把注解里指定的数据源 key 放进去。这个DynamicDataSourceContextHolder内部维护的是一个ThreadLocalDequeString。这里用 Deque双端队列而不是简单的一个变量是为了支持数据源切换的嵌套场景一个方法切到slave后内部又调用了另一个切到report的方法执行完内层方法后应该回到slave而不是直接回到默认数据源。Deque 的 push / poll 机制完美解决了这个栈式切换需求。数据源本身被包装在DynamicRoutingDataSource里。这个类继承了 Spring 的AbstractRoutingDataSource重写了determineCurrentLookupKey()方法方法体就是从当前线程的ThreadLocal里拿出数据源 key然后路由到真正的目标 DataSource。如果拿不到 key就用默认的primary数据源。finally 块里还有一个DynamicDataSourceContextHolder.poll()操作确保方法执行完毕后把当前线程的 key 清掉防止线程池复用线程导致数据源串线。这一步和ThreadLocal的设计配合是多数据源不出乱子的关键保障。我画了一个非常抽象的执行时序配合理解调用orderService.listOrders()该方法标注DS(slave)切面拦截器拦截方法执行push(slave)Mapper 执行查询连接从DynamicRoutingDataSource获取拿到 key 为slave返回对应数据源SQL 在从库执行方法返回切面执行poll()清理ThreadLocal这个机制的精妙之处在于业务代码完全不用关心数据源切换的细节只需要把注意力集中在业务逻辑上。你不用像手动切库那样在每次调用 Mapper 之前手动 set key用完再手动 clear因为切面把这些事都做了。4. 多数据源与事务、连接池的那些坑配置好、注解也加了但真实项目跑起来之后坑才开始浮出水面。这一节我把自己踩过的坑和一些排查技巧完整分享出来这部分是网上教程很少提及的。4.1 事务内外数据源失效与排查这是我在项目里被坑得最惨的一次。当时在 Service 里给一个查询方法加了Transactional和DS(slave)然后发现 SQL 并没有跑到从库上数据始终是从主库查出来的。排查之后原因很清晰Spring 声明式事务是基于连接绑定的。开启事务的那一刻连接就已经从某个数据源获取并绑定到当前线程上了后续所有 SQL 都复用同一个连接。如果Transactional和DS同时标注在同一个方法上事务的创建和切面的数据源切换存在执行顺序问题一旦连接先于数据源切换被获取后续DS怎么改都没用因为连接已经绑定到主库了。解决方案有两个方向第一个方向不要混用 Spring 的Transactional和DS。多数据源场景下如果必须用事务优先使用 dynamic-datasource 提供的DSTransactional注解替代Transactional。这个注解是组件自己实现的本地事务能保证连接和数据源切换的绑定顺序正确。Service public class OrderService { Autowired private OrderMapper orderMapper; DSTransactional DS(master) public void createOrderWithLog() { orderMapper.insert(order); // 同一个事务内写日志表 logMapper.insert(log); } }第二个方向把事务和数据源切换分层。把带事务的方法拆出去让DS标注在 Service 外层方法上事务逻辑放到另一个类中处理。这本质上是用 Spring 的 AOP 执行顺序来解决切换时机问题但代码结构上更绕不如直接用DSTransactional来得干净。不管用哪个方案都要记住一条铁律多数据源下想强一致事务基本只能靠分布式事务方案如 Seata单一的本地事务无法跨库保证一致性。如果是不同数据库实例之间的写入别指望一个DSTransactional搞定一切。4.2 连接池参数调优与多数据源监控多数据源配好之后连接池参数默认都是 HikariCP 的默认值。说实话很多项目跑起来默认值也能凑合但稍微上点量就有问题从库查询慢连接池等待时间拉长接口响应抖动明显。我把实际调优过的参数整理在下面你可以直接参考spring: datasource: dynamic: hikari: # 最小空闲连接数 minimum-idle: 5 # 最大连接数 maximum-pool-size: 20 # 空闲连接超时时间默认10分钟 idle-timeout: 30000 # 连接最大存活时间 max-lifetime: 1800000 # 获取连接超时时间 connection-timeout: 30000 # 连接测试语句 connection-test-query: SELECT 1几个调优心得maximum-pool-size不是越大越好。连接数太多反而会加大数据库的压力我一般先按CPU核数 * 2起步然后根据压测结果增减。如果接口 QPS 高、单次 SQL 很快小池子就够用如果 SQL 慢、耗时长才需要适当调大。max-lifetime最好比数据库的wait_timeout短一点避免连接被数据库服务端提前断开客户端还拿着一个坏连接去执行 SQL。多数据源场景下最好能针对不同数据源分别监控。如果使用 Druid 作为连接池可以打开 Druid 的监控页面HikariCP 没有现成的监控界面但可以通过引入micrometer-register-prometheus配合 Grafana 把hikaricp.connections.pending、hikaricp.connections.active、hikaricp.connections.idle这些指标抓出来看。通不通就看数据库负载和连接池指标。4.3 常见问题速查表一个表帮你省半天排查时间我把平时遇到过的典型问题整理成表排查时照着顺序过一遍大概率能快速定位问题现象可能原因解决方案启动报Failed to configure a DataSource未排除DataSourceAutoConfiguration启动类加excludeDS(xxx)指定了不存在的数据源数据源 key 拼写错误或未配置检查 yml建议开发期strict: trueDS不生效SQL 全部走默认库方法被 Spring 事务代理拦截连接提前绑定改用DSTransactional或将事务与切换分层线程池执行异步任务时数据源串线ThreadLocal未能跨线程传递不推荐跨线程切库必要时手动在任务内部指定动态接口报connection is not available连接池耗尽查看连接池指标调大池容量或优化慢 SQL基于自定义注解的DS失效自定义注解未标记DS元注解确认DS是否加在自定义注解的Target上Mapper 方法上DS不生效Mapper 接口被多次代理确保 Mapper 扫描路径正确避免数据源在事务上下文中提前绑定5. 进阶玩法动态管理数据源、读写分离扩展与批量操作配合多数据源入门之后有些更进阶的玩法值得了解一下。我最近一个项目里就用到了这些能力实测下来收益挺大。5.1 运行时动态新增/移除数据源有些场景下数据源不是固定的可能要在运行过程中动态增加一个新的库。比如租户系统每个新租户可能对应独立的数据库你得能直接在运行时把租户库注册进来而不是每次加库都要重启应用。dynamic-datasource提供了DynamicRoutingDataSource可以通过它手动注册新的数据源。我封装过一个工具类代码大致是这样的Component public class DynamicDataSourceRegistrar { Autowired private DataSource dataSource; public void addDataSource(String key, String url, String username, String password) { DynamicRoutingDataSource ds (DynamicRoutingDataSource) dataSource; HikariDataSource newDs new HikariDataSource(); newDs.setJdbcUrl(url); newDs.setUsername(username); newDs.setPassword(password); newDs.setDriverClassName(com.mysql.cj.jdbc.Driver); ds.addDataSource(key, newDs); } public void removeDataSource(String key) { DynamicRoutingDataSource ds (DynamicRoutingDataSource) dataSource; ds.removeDataSource(key); } }注意一点DataSource注入的时候要小心Spring 容器里可能有两个DataSourceBean一个是DynamicRoutingDataSource一个是默认的。我建议直接用Qualifier(dataSource)来注入确保拿到的是 dynamic 组件注册的路由数据源。运行时新增数据源后代码里就能直接用DS(新key)来切换了。这个能力在配置中心场景下很实用比如在 Apollo 或 Nacos 里把数据库配置发布出来监听配置变更后自动注册新数据源应用全程不用重启。5.2 多数据源场景下配合批量 CRUD 提速很多项目用 MyBatis-Plus 的BaseMapper.insert()一条条插数据性能瓶颈肉眼可见。MyBatis-Plus 3.5.x 提供了insertBatchSomeColumn自定义方法或者IService.saveBatch()批量插入能力配合多数据源的时候需要注意连接一致性。我做批量写入的大致思路是如果批量写入量大先把整个批量操作放在一个方法里方法上标注DS确保整批数据都压到目标数据源。批量操作建议开启rewriteBatchedStatementstrueMySQL JDBC 驱动参数否则批量操作只是看起来批量。不要在一个循环里频繁切换数据源每次切换都有 ThreadLocal push/poll 开销量大了性能下降明显。具体到 MyBatis-Plus 的ServiceImpl它还提供了saveBatch()方法底层实现也是先获取连接然后批量执行。只要能在 Service 层控制好数据源批量效率还是很可观的。5.3 实操过程中的几点建议多数据源配置看起来简单但要想稳定跑上线有几个建议供参考数据源的 key 命名要有规范。不要叫db1、db2而是用业务含义命名比如master、slave、report、user-center一眼能看出这个库是干嘛的。团队里最好在统一文档里维护数据源 key 清单。读写分离不一定非要引入中间件。如果你的项目已经用了这个 dynamic-datasource那读写分离完全可以靠DS(master)和DS(slave)完成。配合spring.datasource.dynamic.primary: master作为兜底即便遗漏了DS注解写入也算安全。多数据源里的连接池参数一定分开配置。不同库的负载特征不一样主库写入频繁、从库查询频繁各自连接池大小可以差异化。dynamic 支持这样配置spring: datasource: dynamic: datasource: master: hikari: maximum-pool-size: 20 slave: hikari: maximum-pool-size: 50这个配置方式我实测是生效的主从库各走各的连接池配置互不影响。最后再分享一个小技巧。我在实际项目里发现多数据源最难的还真不是配置而是团队里的人记不清哪个方法走了哪个库、有没有走错库。建议你把动态数据源开启 SQL 日志打印或者用dynamic-datasource自带的日志输出把每个 SQL 执行时用的数据源 key 打印到日志里。我在项目里就设置过一个 filter请求进来时在日志开头打印当前的DataSourceKey上线排障时一眼就能定位 SQL 落在哪个库里。这个习惯帮我省了至少三次通宵排查的时间强烈建议你也加上。