☰
SpringBoot3多数据源实战:从选型配置到避坑指南
2026/10/10 3:20:06 网站建设 项目流程

做后端这些年,只要业务稍微复杂一点,“一个应用连一个库”的理想状态基本撑不住。用户数据放用户库、订单数据放订单库、日志又要独立一套,再加上读写分离和多租户隔离的需求,所有问题都指向同一个核心:一个SpringBoot应用必须同时操作多个数据源。SpringBoot3发布之后,我把手头几个老项目陆续升级上来,才发现多数据源这件事远比想象中复杂——javax到jakarta的包名变更、自动配置机制的调整、旧版数据源工具的兼容问题,一个接一个往外冒。这篇文章把我真实用过的几种方案、选型背后的逻辑、完整的配置过程和踩过的坑全部串一遍,给正在SpringBoot3上折腾多数据源的你一份可以直接抄作业的参考。

1. 哪些业务真的需要多数据源:先想清楚再动手

很多人一上来就问“多数据源怎么配”,但很少有人先问“我到底为什么需要多数据源”。想清楚这个问题,能帮你少买一半的教训。

1.1 三种最常见的多库场景

第一类是业务分库。举个例子,某个电商项目,用户信息、商品信息、订单数据放在同一个库里,初期确实方便,但一旦并发上来,一个库的连接数、磁盘IO和锁竞争会把所有业务拖垮。于是拆成用户库、商品库、订单库,应用层还要保持一套代码,这时候就必须要有多数据源能力。

第二类是读写分离。主库负责写,从库负责读,读流量是写流量的好几倍。代码层面不需要每个SQL都自己区分走哪个库,而是通过数据源路由规则自动把查询请求分给从库。业务无感,但性能提升明显。某积分系统就是典型的例子,每天上百万的积分流水查询全部走从库,主库负载降了一大截。

第三类是跨库查询与数据隔离。比如运营后台要同时展示用户基础数据和用户行为日志,但这两类数据存放在不同库中,一个接口需要跨库读取。还有一种常见情况是做多租户系统,租户A的数据在A库,租户B的数据在B库,应用启动时根本不知道当前请求属于哪个租户,必须根据上下文动态决定连接哪个数据源。

1.2 多数据源的代价也要提前想清楚

多数据源不是银弹。每增加一个数据源,就增加一份连接管理和事务协调的复杂度。最常见的连环坑有三个:

事务范围变难控制。一个业务方法如果横跨两个数据源,Spring的本地事务就失效了,要么接受数据最终一致,要么引入分布式事务,而分布式事务的代价非常大。

运维和监控变复杂。每个库的连接数、慢查询、性能指标都要分开看,否则出了问题根本不知道是哪一个库导致的。

上线和变更风险更高。原来表结构变更只需要在一个库执行,现在要同步到多个库,版本管理一旦没做好,很容易出现某个库已经更新、另一个库还停留在旧结构的情况。

所以我的建议是:能不分库就不分库,能用单库加索引解决就别急着拆。真的拆了,才认真考虑多数据源的技术选型。别为了炫技把一个单库能解决的问题搞成分布式难题。

2. SpringBoot3时代的多数据源选型:主流方案与取舍

SpringBoot3比较大的变化是JavaEE换成了JakartaEE,所以很多老的多数据源工具在它上面直接跑不起来。实际上面市场上有四条路线可以走,各有各的适用边界。

2.1 原生硬编码:自己配置多个DataSource

这种方案思路最直接。手动定义两个或更多DataSource Bean,再分别为它们创建独立的SqlSessionFactory或JdbcTemplate,然后在使用的地方显式注入对应的实例。

@Configuration public class DataSourceConfig { @Bean(name = "masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "slaveDataSource") @ConfigurationProperties(prefix = "spring.datasource.slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); } }

优点是从代码层面完全可控,没有黑魔法,出了问题一眼能定位。但缺点也很明显:如果库很多,或者切换频繁,代码会膨胀得很难看。两个库各写一套Mapper,甚至要维护两套SqlSessionFactory,这种方案更适合数据源数量极少、切换场景基本固定的项目。维护成本高,我一搬不推荐业务复杂的时候用。

2.2 Spring原生AbstractRoutingDataSource

Spring本身提供了AbstractRoutingDataSource这个抽象类,它本质上是一个路由中转站。我们可以在运行时通过determineCurrentLookupKey()方法返回一个键,路由数据源会根据这个键选择真正对应的目标数据源。

核心逻辑是:

public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceKey(); } }

使用ThreadLocal保存当前线程的路由键。当请求进来时,在切面中设置key,业务执行完后再清理掉。

这套方案比硬编码灵活很多,数据源可以按需扩展,不用每个数据源都写死一套Mapper。但抽象类只解决“路由”的问题,事务管理、分页插件识别、自动配置适配等问题全都得自己处理。也就是说,它离“开箱即用”还有距离,适合喜欢定制、愿意自己造轮子的团队。我用过一次,后来发现需求一变就要改切面,维护成本不低。

2.3 dynamic-datasource:轻量级动态数据源的主流之选

这应该是国内目前用得最广泛的开源数据源路由方案。它基于AbstractRoutingDataSource做了封装,把AOP切面、注解解析、连接切换、事务增强全部内置了。用户只需要在方法或类上加一个@DS注解,方法执行前自动切到指定数据源,方法结束后自动切回主数据源。

它的核心使用方式非常直观:

@Service public class OrderService { @DS("order-db") public List<Order> getOrderList() { return orderMapper.selectList(); } }

在SpringBoot3下需要引入专门适配的starter,后面会详细演示。这个方案的优点是轻量、上手快、代码侵入量小,还有日志打印当前数据源key,排查问题看得清楚。缺点就是它只解决多数据源“切换”这件事,如果还需要分库分表、分布式事务,得配合其他组件。

2.4 ShardingSphere:需要分库分表时的重量级选手

当业务量已经大到需要分库分表,比如订单表单表过亿,那就不是单纯的多数据源问题,而是数据分布策略的问题。ShardingSphere这类组件会用一套配置规则决定SQL应该路由到哪个库、哪张表,它比多数据源框架的层级更高。

ShardingSphere提供了数据分片、读写分离、数据脱敏、分布式事务等一整套能力。同样,它也更复杂,学习成本和运维成本都明显高于前几个方案。如果只是想在两个库之间切来切去,用ShardingSphere就是把大炮打蚊子,严重过度设计。

2.5 选型对比与决策建议

对比维度原生多DataSourceAbstractRoutingDataSourcedynamic-datasourceShardingSphere
上手成本低中低高
代码侵入高,每多一个库写一套Bean中,需要自研切面极低,注解驱动低,配置驱动
切换灵活性差好非常好非常好
事务支持需要自管需要自管提供@DSTransactional自带分布式事务
分库分表不支持不支持不支持支持
SpringBoot3适配需自己确认需自己确认有专门starter有专门starter
适用场景数据源极少且固定喜欢定制的中型项目绝大多数业务系统数据量大且需分片

我的建议是:普通业务系统优先考虑dynamic-datasource,它把99%的多数据源切换需求都覆盖了,文档和资料都比较全。如果团队习惯原生写法且数据源不超过两个,用AbstractRoutingDataSource自己包一层也可以。真到了必须分库分表的阶段,直接引入ShardingSphere,尽量别自己造分片轮子。做技术选型时,把“最少代码完成最多需求”作为第一原则,多数的纠结都会变得明朗。

3. 实操落地:SpringBoot3 + dynamic-datasource从零配置

既然当前多数场景下dynamic-datasource是性价比最高的选择,我就用这套方案完整演示一遍从引入依赖到跑通接口的整个过程。版本基于SpringBoot3,这一点非常关键。

3.1 依赖引入与SpringBoot3适配

SpringBoot3最大的坑是javax变成了jakarta,导致很多老框架的旧版本无法直接使用。dynamic-datasource在3.x版本时代是为SpringBoot2设计的,到了SpringBoot3必须引入专门提供适配的starter。

我当前项目使用的方式是:

<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <version>4.2.0</version> </dependency>

注意artifactId里一定要有spring-boot3字样。引入这个之后,MyBatis相关依赖也尽量采用mybatis-plus或mybatis-spring的SpringBoot3适配版本,否则运行期容易因为类加载顺序问题出现各种奇怪异常。我就曾经因为MyBatis版本太老,导致数据源初始化时报“Error creating bean with name 'sqlSessionFactory'”,排查了一个晚上才发现是版本兼容问题。

3.2 多数据源核心配置与代码实现

先看配置文件。假设业务需要连接两个库,分别是主库master-db和订单库order-db:

spring: datasource: dynamic: primary: master strict: true datasource: master: url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver order-db: url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

primary指定默认数据源,严格模式strict如果设为true,使用一个不存在的@DS名称会直接抛异常,它能帮你尽早暴露配置写错的问题,所以我建议生产环境一定开strict。

然后写一个Mapper,放在一个独立的包下:

@DS("order-db") @Mapper public interface OrderMapper { List<Order> selectOrderList(); }

这个@DS加在接口上,表示这个Mapper的所有操作都走order-db。如果某个方法需要临时走主库,可以在方法上再加@DS覆盖类上的配置。

Service层同样可以精确控制:

@Service public class OrderQueryService { private final OrderMapper orderMapper; public OrderQueryService(OrderMapper orderMapper) { this.orderMapper = orderMapper; } @DS("order-db") public List<Order> queryFromOrderDb() { return orderMapper.selectOrderList(); } @DS("master") public Order queryFromMaster(Long id) { return orderMapper.selectById(id); } }

实测下来,dynamic-datasource的AOP切换是非常精准的:进入方法时把key放入ThreadLocal,方法结束时移除key。但要注意,如果你在一个Service方法里调用了另一个类的方法,并且对方也标了@DS,那切换规则会以后者为准。这种“隐式覆盖”很容易让人误判,排查时一定要结合日志中打印的“dynamic-datasource switch to ...”信息来确认实际走到了哪个库。

3.3 事务边界与@DSTransactional的正确用法

多数据源最容易搞出事故的就是事务。Spring的@Transactional基于DataSourceTransactionManager,事务管理器在启动时就绑定了数据源。如果你在方法上同时标了@Transactional和@DS,指望事务方法内自由切换数据源,那基本是白日做梦。

来看一个典型反例:

@Transactional @DS("order-db") public void updateOrderAndUser() { orderMapper.updateOrder(); userMapper.updateUser(); // 这个Mapper默认走master }

实际运行后,@DS会先把数据源切到order-db,但@Transactional准备工作拿连接时,拿到的连接只对应order-db。userMapper虽然本身配置走master,但因为当前线程已经绑定了order-db的连接,所以它的SQL依然会发往order-db,数据落到了错误的库里。

解决方案是在跨数据源操作的场景下不要使用Spring原生事务,改成dynamic-datasource提供的@DSTransactional:

@DSTransactional public void updateOrderAndUser() { orderMapper.updateOrder(); userMapper.updateUser(); }

@DSTransactional内部自己管理多数据源连接,能让同一个方法里不同Mapper分别拿到各自对应的连接,然后再统一提交或回滚。它本质上是通过自定义事务控制来实现多库一致性,但同时也要说明:这不等于分布式强一致性事务,如果对一致性要求极高,比如资金相关场景,还是得考虑引入可靠的消息或分布式事务组件。

另外一个更容易被忽视的点是:由于@DSTransactional自己接管了事务生命周期,它不支持在同一个方法中嵌套调用另一个标有Spring原生@Transactional的方法,否则事务控制会出现重叠,回滚行为无法预料。团队里如果有新同学,建议在规范里明确:多数据源场景一律用@DSTransactional,原生@Transactional只留给单数据源场景。

4. 多数据源避坑实录:那些文档没写清楚的细节

配置能跑通只是第一步,真正熬人的是上线之后的疑难杂症。这些坑我基本都踩过,每一条都是拿线上事故换来的经验。

4.1 事务内切换数据源为什么会失效

这个前面已经展开讲过,再补充一个更隐蔽的例子。即使你的方法没有加@Transactional,如果在同一个线程里,某个框架组件内部偷偷绑定了连接,也会导致@DS切换失效。比如Spring事件监听器里拉取数据、定时任务里跨方法调用、或者在一个已有的业务事务回调里继续执行SQL。

我遇到过的一个Case是:一个订单导出功能,在一个被@Transactional包裹的大事务里调用了一个工具类,工具类里的查询标了@DS("order-db"),结果这个查询还是落到了主库。原因是外部事务已经通过主数据源连接执行过SQL,连接被绑定到了当前线程,@DS切换只能改路由键,但要拿新连接时发现线程已经有主库连接,于是直接复用了。排查方法很简单,打开dynamic-datasource的日志,观察每一个SQL执行前打印的数据源key,对比实际连接的数据库名。一旦发现key切了但连接没换,就去查方法的调用链,看是否掉进了Spring事务包裹的范围。

4.2 分页插件、主键生成与连接池的坑

分页是另一个高频翻车点。MyBatis的分页插件PageHelper,在单数据源下很好用,但多数据源下如果设置方言不正确,或者分页插件先于路由初始化,就会导致分页SQL发到了错误的数据源上。SpringBoot3下记得使用适配版本,并显式指定数据库方言。

主键生成也需要留意。不同数据源的主键策略并不一样。某次我把一个旧项目从单库拆成用户库和订单库,结果订单库的表还沿用了自增ID,而用户库已经改成了雪花ID。虽然应用层没报错,但后续合并报表时两边ID语义完全不一致,导致数据对不上。在多数据源架构里,建议所有业务表统一使用分布式ID生成策略,避免把数据源的主键特性写死在业务代码里。

连接池方面,dynamic-datasource本身不负责创建连接池,它会用配置中指定的连接池实现。SpringBoot3默认HikariCP,如果后续想用Druid,需要单独引入适配依赖,并且把配置项从spring.datasource.hikari改为spring.datasource.druid开头。混用多个连接池实现也可以,但会显著增加排查难度,我个人的经验是统一用一种连接池。

4.3 初始化顺序与自动配置失效的排查

SpringBoot3的自动配置机制和SpringBoot2差别不小,老项目升级上来后经常遇到的一个现象是:某些Bean已经定义但就是不生效,或者报“Failed to configure a DataSource”错误。

这个报错通常意味着Spring没有找到可用的DataSource。在多数据源项目中,一个比较常见的原因是配置了dynamic-datasource,却又额外引入了一个不相关的DataSource自动配置类,比如某些第三方starter会偷偷创建一个DataSourceCreator。SpringBoot3下如果不小心引入了一个为SpringBoot2设计的旧版starter,它的自动配置类在spring.factories里声明,新版根本不会读取,于是预期该生效的DataSource没有创建。

排查这类问题,我一般的步骤是:

先看启动日志中是否有AutoConfiguration相关的排除记录。

再看spring.datasource.dynamic配置是否真正被加载,如果无法确认,加一个@ConfigurationProperties注册类,把配置绑定结果打印出来。

最后检查依赖树,找出那些间接引入的DataSource相关包。

记得有一次,项目里只是多了一个报警组件依赖,它的自动配置抢先创建了一个空DataSource,导致整个应用启动报错。解决办法是在启动类上用exclude属性排除那个自动配置类,或者干脆去掉多余依赖。这类问题往往比业务代码bug更难发现,因为它看不见摸不着,只能靠依赖分析和日志层层扒。

5. 常见故障排查速查表

多数据源的问题花样多,但规律性也强。我把高频故障按表现、可能原因和解决思路整理成了一张速查表,排查时可以直接对照。

5.1 典型报错与解决思路

故障现象常见原因解决思路
启动报Failed to configure a DataSource多数据源配置未生效,或第三方starter抢先创建了DataSource检查依赖树,排除多余的DataSource自动配置类,确认dynamic配置被加载
@DS注解不生效,SQL全部落主库AOP未被拦截,类没有被Spring管理,或方法自调用确认Mapper/Service是注入的Bean,避免同类内部方法直接调用,改用注入对象
事务里切库失败,数据落错库外部@Transactional绑定了旧连接,ThreadLocal连接被复用改成@DSTransactional,或调整事务边界
分页SQL方言错误或无法分页PageHelper版本不适配SpringBoot3,或方言未配置升级适配版本,显式设置方言,检查拦截器顺序
同一数据源连接池爆满数据源连接数配置过小或慢SQL过多分开监控每个库的连接池,优化慢SQL,调大max-pool-size
切换数据源后查询报“表不存在”路由到的库确实没有对应表,或者表结构不一致核对@DS里的key与配置中的数据源名称,检查库表结构
主从延迟导致刚写入读不到读写分离场景下从库同步延迟关键链路强制走主库,或引入短时缓存

这张表只是排查的起点,真正的难点在于日志里往往不会直接告诉你“我是因为连接被绑定才选错库”。还是那句老话,先用日志定位“当前SQL实际连到了哪个库”,再倒推为什么。

5.2 我常用的排查路径

排查多数据源问题,我有一套固定动作,效率比乱试配置高很多。

打开dynamic-datasource的日志级别,调整为debug,观察运行时打印的switch日志。这一步能最快确认路由是否如预期执行。

在容易出问题的方法里临时加一行日志,打印当前数据源key和目标SQL的表名,确认代码层面的意图。

如果怀疑是事务连接绑定,可以用DataSourceUtils.getConnection把当前线程绑定连接打印出来,对比它的jdbcUrl。但这步只能临时排查,不能长期保留在代码里。

最后才去查依赖版本和自动配置,大多数时候问题都会在前两步就水落石出。这套顺序的核心逻辑是:先证明代码走到了哪一步,再看环境配置为什么没跟上,而不是颠倒过来瞎猜。

6. 写在最后:几条实战心得

多数据源这件事,配置本身只是冰山一角,真正的复杂度在事务边界、连接管理和团队约定上。我个人的体会是,最容易出问题的不是新手不会写@DS,而是老项目在重构时把多数据源强塞进了一个原本单数据源的事务模型中,结果上线即事故。

如果让我给几个关键建议,第一是所有数据源名称和业务归属必须在文档里写清楚,不能只存在代码注解里。第二是全国统一使用一种连接池实现,别这个库Hikari那个库Druid,出问题时排查成本和复杂度都会成倍增长。第三是上线前一定要补一个跨数据源查询的集成测试,把主从路由、事务回滚、分页查询三条链路全部覆盖到。

多数据源方案选定后,后续还可以继续扩展多租户隔离、读写分离、分片路由等能力,当然这些都是后话。先把最小可运行的方案跑通,再一步步加固,这条路走起来会稳得多。

最后再分享一个小技巧:给动态数据源开启日志后,在开发环境把输出格式做成一行一个关键字段,比如“当前线程ID + 数据源key + SQL摘要”,这样无论是自己调试还是让同事帮忙排查,都能省掉大量沟通成本。这个习惯我保留了很久,也让我少背了好几次锅。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询