news 2026/10/2 14:50:36

SpringBoot+MyBatis-Plus多数据源配置实战:@DS注解开箱即用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+MyBatis-Plus多数据源配置实战:@DS注解开箱即用

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.x,JDK 1.8。如果你用的是 Spring Boot 3.x,记得把 dynamic-datasource 的依赖换成dynamic-datasource-spring-boot3-starter,整体包名和用法基本一致。

2.1 引入依赖与版本说明

在pom.xml里,加上dynamic-datasource-spring-boot-starter这个依赖:

<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>4.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?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://127.0.0.1:3306/readonly_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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 List<User> listAll() { return userMapper.selectList(null); } }

放在 Service 方法上:只对方法生效,类上没有@DS时,老方法走默认数据源,新方法走指定数据源。

@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Override @DS("slave") public List<Order> listOrders() { return orderMapper.selectList(null); } @Override @DS("master") public void createOrder(Order order) { orderMapper.insert(order); } }

放在 Mapper 接口方法上:直接控制 Mapper 层面的数据源,适合 Mapper 之间明确分库的场景。

public interface UserMapper extends BaseMapper<User> { @DS("slave") List<User> 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内部维护的是一个ThreadLocal<Deque<String>>。这里用 Deque(双端队列)而不是简单的一个变量,是为了支持数据源切换的嵌套场景:一个方法切到slave后,内部又调用了另一个切到report的方法,执行完内层方法后应该回到slave,而不是直接回到默认数据源。Deque 的 push / poll 机制完美解决了这个"栈式"切换需求。

数据源本身被包装在DynamicRoutingDataSource里。这个类继承了 Spring 的AbstractRoutingDataSource,重写了determineCurrentLookupKey()方法,方法体就是从当前线程的ThreadLocal里拿出数据源 key,然后路由到真正的目标 DataSource。如果拿不到 key,就用默认的primary数据源。

finally 块里还有一个DynamicDataSourceContextHolder.poll()操作,确保方法执行完毕后把当前线程的 key 清掉,防止线程池复用线程导致数据源串线。这一步和ThreadLocal的设计配合,是多数据源不出乱子的关键保障。

我画了一个非常抽象的执行时序,配合理解:

  1. 调用orderService.listOrders(),该方法标注@DS("slave")
  2. 切面拦截器拦截方法,执行push("slave")
  3. Mapper 执行查询,连接从DynamicRoutingDataSource获取,拿到 key 为slave,返回对应数据源
  4. SQL 在从库执行
  5. 方法返回,切面执行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启动类加exclude
@DS("xxx")指定了不存在的数据源数据源 key 拼写错误或未配置检查 yml,建议开发期strict: true
@DS不生效,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,确保整批数据都压到目标数据源。
  • 批量操作建议开启rewriteBatchedStatements=true(MySQL 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 落在哪个库里。这个习惯帮我省了至少三次通宵排查的时间,强烈建议你也加上。

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

华中科技大学网络空间安全学院信息系统安全实验源码与说明书解析

简介&#xff1a;这份资源是华中科技大学网络空间安全学院信息系统安全实验的完整课程资料&#xff0c;面向高校网络安全、信息安全相关专业学生及自学者&#xff0c;适合在课程设计、课程实验场景中动手实践。压缩包共73个文件&#xff0c;约4.65MB&#xff0c;以C语言源码、P…

作者头像 李华
网站建设 2026/10/2 14:49:49

Neural Harmonic Measure Operator:PDE边界建模新范式

1. 这不是又一个Transformer变体&#xff1a;Neural Harmonic Measure Operator到底在解决什么问题&#xff1f;你可能刚刷到“Neural Harmonic Measure Operator”这个词&#xff0c;第一反应是——又一个带“Neural”和“Operator”的新模型&#xff1f;名字听着像论文里随手…

作者头像 李华
网站建设 2026/10/2 14:49:19

知识蒸馏为何扛不住强化学习攻击?攻防实战解析

1. 项目概述&#xff1a;当知识蒸馏遇上强化学习&#xff0c;防御为何“一碰就碎”“Distillation Defenses Easily Break After Reinforcement Learning”——这个标题乍看像一篇论文的结论句&#xff0c;实则直击当前模型安全领域一个被严重低估的现实困境&#xff1a;我们花…

作者头像 李华
网站建设 2026/10/2 14:49:08

Paperclip 实战:Node.js + React 构建能思考能行动的 AI Agent

1. 从 paperclip 这个名字说起&#xff1a;它到底想解决什么问题第一次看到paperclip这个项目名&#xff0c;我脑子里蹦出来的不是回形针&#xff0c;而是那个经典的“回形针助手”梗——一个总想帮你把事情办完的小东西。放到 AI agent 的语境里&#xff0c;这个名字其实挺贴切…

作者头像 李华
网站建设 2026/10/2 14:48:17

单索引Bandit:高维动作下的结构反演与决策流形导航

1. 这不是传统 Bandit&#xff0c;而是一场关于“信息提取”与“决策空间建模”的精密手术单索引带臂问题&#xff08;Single-Index Bandits&#xff09;这个标题乍看像一篇纯理论论文&#xff0c;但如果你在推荐系统、临床试验设计、或工业级自适应实验平台里摸爬滚打过几年&a…

作者头像 李华
网站建设 2026/10/2 14:47:57

二叉树递归进阶:平衡、路径、左叶子与完全二叉树计数

代码随想录训练营刷到第13天&#xff0c;二叉树模块终于开始动真格的了。今天的四道题——110.平衡二叉树、257.二叉树的所有路径、404.左叶子之和、222.完全二叉树的节点个数——没有引入任何新概念&#xff0c;用到的全是前面几天反复练习过的递归模板和遍历顺序&#xff0c;…

作者头像 李华