资讯动态

SpringBoot多数据源配置实战:从原理到动态路由落地

发布时间:2026/9/10 3:21:06 来源:尧图企业网站定制
我先自己确认一下这次要写的是一篇关于 SpringBoot 多数据源配置的实战经验分享内容完全围绕技术本身展开不涉及任何敏感话题安全合规没问题。下面直接开始标题编号、代码、实操细节都给到位确保是能直接落地的干货。 SpringBoot 多数据源这个需求接私活和做企业项目都特别常见。不管你是要读写分离还是单纯把报表库、日志库从主业务库里拆出来一开始的思路不捋清楚后面配置完了跑起来各种诡异报错能把人折磨到怀疑人生。这篇文章就把我之前在项目里整理的完整方案拿出来从场景分析到核心代码到坑位排查一条龙讲清楚。如果你是刚接触 SpringBoot 不久正好在准备 springboot 面试题或者正在做的管理系统要连两个数据库这篇内容应该能直接给你抄作业的底气。1. 多数据源场景与方案选型先说清楚什么时候真的需要多数据源以及到底有哪几条路可以走。1.1 常见需求场景拆解很多初学者一听“多数据源”就觉得是高深玩意儿其实它就是“一个应用要访问多个数据库”。我梳理了实际项目里最常见的几类场景排名不分先后但都是真实踩出来的表格里列得简洁但实际选型就要思考更深一层。比如读写分离你虽然可以把读库和写库拆成两个数据源但如果只是想让查询走从库、写入走主库用 AbstractRoutingDataSource 做动态切换是最轻量的方案。而如果是两个完全独立、表结构都不同的业务库那就更适合用 MyBatis-Plus 多数据源插件代码里加个注解就能隔离维护起来也直观。场景典型需求推荐方案读写分离主库写、从库读降低主库压力AbstractRoutingDataSource 动态路由独立业务库不同模块物理隔离比如订单库和用户库MyBatis-Plus 的 DS 注解报表/日志库高频查询和历史归档数据不在主库独立 SqlSessionFactory 硬隔离第三方系统对接需要直接读取第三方库的表数据只读数据源 连接池隔离1.2 三种主流实现方案对比多数据源的实现思路我总结下来无非是下面这三条路线。方案A用 AbstractRoutingDataSource 做动态数据源路由Spring 内置提供了一个 AbstractRoutingDataSource 抽象类它内部维护了一个目标数据源 Map通过 determineCurrentLookupKey() 方法返回的 key 来决定当前线程用哪个数据源。你可以基于它封装一个动态路由组件配合 ThreadLocal 存储当前线程的 key再用 AOP 切面在 Service 方法执行前切换 key方法执行完再清理。这个方案最灵活自由度最高。读写分离、多租户隔离都能做但所有代码逻辑都要自己写工程复杂度可控但细节多。方案BMyBatis-Plus 提供的 dynamic-datasource 插件如果项目用了 MyBatis-Plus那直接用 dynamic-datasource-spring-boot-starter 这个官方插件是最省事的。它封装了所有切换逻辑只需要在 application.yml 里配置多个数据源然后在 Mapper 或 Service 方法上加 DS(dsName) 注解就能完成切换。这个方案对业务代码入侵最小代码上就是一个注解的事。但它强依赖 MyBatis-Plus如果项目没用 MyBatis-Plus引这个插件就有点大炮打蚊子了。方案C配置多个 SqlSessionFactory 实现物理隔离这种方案等于给两个数据库各建一套独立的 MyBatis 基础设施包括各自的 SqlSessionFactory、DataSource、SqlSessionTemplate、MapperScan。两套体系互不干扰隔离性最强适合那种两个库的表结构完全独立、业务上没有交叉的模块。缺点也很明显代码里不能自由切换数据源事务跨库操作基本没法做。而且 Mapper 接口要按包名分开扫描维护成本高容易产生重复配置代码。我在实际项目里最常用的还是方案A和方案B。如果是从零开始的新项目且用了 MyBatis-Plus我推荐 B省心。如果是老项目改造或者不想引入额外依赖那就用 A可控性最强。这篇文章主要展开讲 A 的完整落地过程因为理解了它的路由原理B 的很多细节你也能一眼看穿。2. 环境准备与前置依赖方案定了接下来就是准备环境、配置依赖。2.1 依赖导入与版本说明我用的环境是 SpringBoot 2.7.x MyBatis-Plus 3.5.x MySQL 8.0这些版本的组合测试了很久稳定可靠。如果你的项目是 SpringBoot 3.x需要注意 javax 命名空间变成了 jakarta自定义数据源配置类里的 import 语句要相应调整。pom.xml 里最小依赖集合是这样的dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependencyspring-boot-starter-aop 别漏了动态数据源切换的核心逻辑要靠 AOP 切面来实现少了这个依赖你后面写的自定义注解就完全不会生效。有一点要特别提醒如果用了 MyBatis-Plus就别再引入 mybatis-spring-boot-starter 了不然两个 MyBatis 的自动配置会打架报错信息还特别隐晦极难排查。踩过一次这个坑之后我现在写 pom.xml 都会格外注意依赖冲突问题。2.2 数据库准备为了方便演示我准备了两个库ds_master 作为主库ds_slave 作为从库/业务库。两个库都建一张 user 表但写入不同的初始数据这样切换数据源查询时能直观看到效果。-- 主库 CREATE DATABASE ds_master DEFAULT CHARACTER SET utf8mb4; USE ds_master; CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(50) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO user (name) VALUES (master_zhangsan), (master_lisi); -- 从库 CREATE DATABASE ds_slave DEFAULT CHARACTER SET utf8mb4; USE ds_slave; CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(50) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO user (name) VALUES (slave_zhangsan), (slave_lisi);两个库里的数据区分得很明显之后你一运行查询返回的是 master_ 还是 slave_ 开头就知道当前路由到了哪个数据源。3. 核心实现基于 AbstractRoutingDataSource 的动态路由这一部分是整篇文章最核心的内容理解了数据源切换原理你就掌握了多数据源配置的底层逻辑。别慌我一步步拆开揉碎给你讲。3.1 原理先讲透Spring 是怎么知道用哪个数据源的先从宏观上理解 Spring 的数据源加载流程。SpringBoot 启动时如果没有特殊配置会读取 application.yml 里 spring.datasource 下的配置自动装配一个 HikariDataSource。但在多数据源场景下我们要打破这个默认行为不再让 SpringBoot 自动创建数据源而是我们自己创建多个数据源再交给一个“路由数据源”来管理。这个“路由数据源”就是 AbstractRoutingDataSource。它内部维护了一个 MapObject, Object targetDataSourceskey 是数据源标识value 是实际的数据源对象。当代码里要获取连接时AbstractRoutingDataSource 会调用它的抽象方法 determineCurrentLookupKey()拿到当前线程对应的 key然后从 Map 里取对应的真实数据源再从这个真实数据源里 getConnection()。这个过程有点像驿站送信你手上有很多驿站目标数据源每个驿站对应一个地址编号key。每一次送信获取数据库连接前得先查下表determineCurrentLookupKey看这封信该送到哪个驿站。动态数据源的所有设计本质就是在维护“当前这封信该送到哪个驿站”这个信息。那如何让每个线程都有自己的 key 呢答案是 ThreadLocal。每个线程往 ThreadLocal 里放自己的数据源标识AOP 切面在进入 Service 方法前放进去方法执行完在 finally 里清理掉。这样多线程并发时各线程的数据源互不干扰。3.2 数据源上下文管理类DataSourceContextHolder先写一个简单的 ThreadLocal 封装负责保存和获取当前线程的数据源 keypublic class DataSourceContextHolder { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSource(String dataSourceKey) { CONTEXT_HOLDER.set(dataSourceKey); } public static String getDataSource() { return CONTEXT_HOLDER.get(); } public static void clearDataSource() { CONTEXT_HOLDER.remove(); } }代码很简单但作用很关键。这里有一个重要细节清理数据源时一定要用 remove() 而不是 set(null)。因为线程池里的线程会被复用如果你只 set(null) 而不 remove之前设置的 key 可能残留在线程里导致下一次请求路由到错误的数据源。这种 Bug 是间歇性出现的排查起来非常抓狂。我在生产环境就遇到过这种问题正常跑几小时没问题某几个接口突然就查询到了别的库的数据。后来靠线程池线程 ID 和请求日志一点一点排查才发现是 ThreadLocal 没清干净。所以这里多啰嗦一句清理用 remove()不要用 set(null)。还要解释一个疑问为什么不用 synchronized 或者全局变量因为高并发下不同线程要访问不同的数据源。用全局变量会互相覆盖用 synchronized 会阻塞其它线程ThreadLocal 恰好是“线程隔离”的标准解决方案。3.3 动态数据源路由类DynamicDataSource接下来创建一个类继承 AbstractRoutingDataSourcepublic class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }就这么几行代码但它是整个方案的心脏。AbstractRoutingDataSource 在初始化时会调用 afterPropertiesSet() 方法将我们传入的 targetDataSources 解析并放好。每次 getConnection() 时都会先走 determineCurrentLookupKey()拿到当前线程的数据源 key再去 Map 里取真正的数据源。这个方法返回 null 也没关系AbstractRoutingDataSource 会使用默认数据源defaultTargetDataSource。所以我的习惯是把主库设为默认数据源这样即使某处忘记加切换逻辑至少读写不会走到从库上去保证核心业务安全。3.4 自定义注解与 AOP 切面数据源切换的入口光有路由类还不够得有个东西告诉它“什么时候切到哪个数据源”。我选择自定义注解 AOP 切面的方式这也是可读性和维护性最好的方式。先定义注解Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DS { String value() default master; }然后定义切面Aspect Component public class DataSourceAspect { Around(annotation(ds)) public Object around(ProceedingJoinPoint joinPoint, DS ds) throws Throwable { DataSourceContextHolder.setDataSource(ds.value()); try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clearDataSource(); } } }这个切面拿到的 ds.value() 就是要切换的数据源标识。在方法执行前设置执行后清理确保线程不残留。AOP 切面拦截的本质是 Spring AOP 动态代理。有一点需要注意由于 Spring AOP 默认是基于接口代理的所以这个切面只能拦截 Spring 管理的 Bean 的方法调用。如果你在同一个类里一个方法调另一个方法切面是不会生效的因为调用没有经过代理对象。这是 Spring AOP 的基本特性也是很多人切换数据源失败的根本原因之一。我自己就踩过这个坑Service 里有一个公共方法调用了本类中加了 DS 注解的另一个方法结果数据源切换死活不生效查了半天才发现是方法自调用问题。3.5 数据源配置类把多个数据源注册给路由源这一步要把多个数据源创建出来注册到 DynamicDataSource 里并且接管 SpringBoot 的自动数据源配置。Configuration public class DataSourceConfig { Bean ConfigurationProperties(prefix spring.datasource.master) public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } Bean ConfigurationProperties(prefix spring.datasource.slave) public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); } Bean Primary public DynamicDataSource dynamicDataSource( Qualifier(masterDataSource) DataSource masterDataSource, Qualifier(slaveDataSource) DataSource slaveDataSource) { MapObject, Object targetDataSources new HashMap(); targetDataSources.put(master, masterDataSource); targetDataSources.put(slave, slaveDataSource); DynamicDataSource dynamicDataSource new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(masterDataSource); return dynamicDataSource; } }DataSourceBuilder.create().build() 会根据配置文件的属性自动推断连接池类型。只要能找到 HikariCP 依赖默认就是 HikariDataSource。Primary 注解是关键它告诉 Spring 这是首选的 DataSource Bean否则你会遇到“expected single matching bean but found 2”的错误。有一个细节要额外注意ConfigurationProperties(prefix spring.datasource.master) 这种方式要求配置项不能写在 SpringBoot 默认的 spring.datasource 节点下而是写在自定义的 spring.datasource.master 和 spring.datasource.slave 子节点下。否则的话SpringBoot 自动配置机制会和你的自定义配置发生冲突容易导致数据源被重复创建或者配置项读取不到。我用这个方案配置好了之后还要把 MyBatis-Plus 的 SqlSessionFactory 创建交给动态数据源。其实大多数情况下只要动态数据源是 Primary 的 DataSourceMyBatis-Plus 会自动拿到这个路由数据源来创建 SqlSessionFactory动态切换就能自动生效。不需要额外再写 SqlSessionFactory 的配置这也是这个方案轻量化的一个点。3.6 application.yml 配置对应上面代码配置文件这样写spring: datasource: master: jdbc-url: jdbc:mysql://localhost:3306/ds_master?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver slave: jdbc-url: jdbc:mysql://localhost:3306/ds_slave?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver注意这里用的是 jdbc-url 而不是 url。如果使用 HikariCP 作为连接池它会把 url 当作一个普通属性而不会正确解析为 JDBC URL导致连接失败。当然如果不用 DataSourceBuilder而是直接 new HikariDataSource() 然后 setJdbcUrl那配置项的命名就无所谓了。但既然用了 DataSourceBuilder还是老老实实写 jdbc-url 最稳妥。这里需要提醒一个核心逻辑key 的名称必须与 DS 注解的 value 值保持一致。尤其在切面里如果你写 DS(master)那 Map 里的 key 必须是 master。大小写、空格有一处不一致切换到不存在的数据源 key 时会抛出无法确定数据源的异常。所以通常我会定义一个常量类统一管理数据源名称避免手写字符串导致的小错误。4. 实际使用与事务边界问题代码配置完了怎么在业务里用起来以及事务对数据源切换有什么影响这一节说清楚。4.1 Service 层使用示例直接用注解切换代码清爽Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override DS(master) public ListUser listMasterUsers() { return userMapper.selectList(null); } Override DS(slave) public ListUser listSlaveUsers() { return userMapper.selectList(null); } }运行后listMasterUsers() 返回的是 ds_master 库里的 user 数据listSlaveUsers() 返回的是 ds_slave 库里的 user 数据。说明数据源切换成功。刚入门的读者可以这样理解加了 DS 注解的方法就相当于在方法执行前调用了 DataSourceContextHolder.setDataSource(slave)方法结束后自动调用 clearDataSource()。这个切换对业务代码完全透明你不需要在 Mapper 层写任何与多数据源相关的代码。还可以给 Mapper 接口加注解。当 DS 同时出现在类和方法的注解上时方法上的注解优先级更高覆盖类级别的配置。利用这个特性你可以把默认库配在类上特殊方法再单独指定库。4.2 事务机制对数据源切换的影响这是多数据源方案里最暗藏的坑也是面试官最喜欢追问的点。Spring 的 Transactional 是基于 AOP 的它会在方法开始前开启事务然后整个方法的数据库操作都在这个事务里执行。问题来了事务管理器和数据源是绑定的。Spring 开启事务时会从当前数据源获取一个连接并且这个连接在整个事务期间都被绑定到当前线程即使你中途用 DS 切换了数据源 key事务内已经持有的连接也不会变。通俗点说事务像一个大袋子先把数据源A的连接装进去了。你后续切到了数据源B但是事务袋子里的连接还是数据源A的查询仍然会落在数据源A上。所以结论是DS 和 Transactional 不能简单地同时用在同一个方法上尤其当你需要在一个事务里操作两个不同数据源的时候。这两种机制没法直接实现跨库事务的一致性强一致性需要引入分布式事务框架比如 Seata。但如果只做单数据源上的事务切分好边界即可。我的一些经验总结写操作和读操作分别放在不同方法里写方法上加 DS(master) 和 Transactional读方法上加 DS(slave)不加事务或只加只读事务。某个方法同时需要操作两个库的数据比如先从主库查订单再写到从库的日志表不要在一个方法里同时切换。建议拆成多个方法每个方法指定一个数据源外层用编程式事务来控制整体逻辑。尽量让方法的事务边界和数据库连接的使用边界保持一致这是避免这类问题的最基本法则。4.3 编程式事务在跨库场景中的应用如果确实需要在一个业务里连续操作两个数据源比如先从主库扣库存再向从库写操作日志并且要求“库存扣减成功才写日志”这时候可以把事务控制改为编程式。让每个数据源的操作各自独立提交通过业务逻辑来保证最终一致性Autowired private TransactionTemplate transactionTemplate; public void businessOperation() { // 操作主库 transactionTemplate.execute(status - { orderMapper.updateStock(); return null; }); // 操作从库独立事务 transactionTemplate.execute(status - { logMapper.insertLog(); return null; }); }注意这只是牺牲了强一致性换取性能的折中方案。扣库存和写日志中间如果进程崩了还是会出现两边数据不一致的情况。真正要跨库强一致得上分布式事务这块内容展开又是一篇长文这里先点到为止。如果你的面试官问“多数据源下事务怎么保证”你能说出上面这段分析基本能拿高分。5. 连接池与性能调优细节数据源切换只是基础实际应用里连接池的配置直接决定系统的稳定性和并发表现。5.1 连接池参数怎么调我用了 HikariCP它默认配置已经很优秀但有几个参数值得根据场景自定义参数默认值我的推荐说明maximum-pool-size1030-50连接池最大连接数按接口 QPS 估算minimum-idle105-10最小空闲连接数太低会频繁创建连接connection-timeout3000030000获取连接超时时间等太久直接失败idle-timeout600000600000空闲连接存活时间max-lifetime18000001800000连接最大生命周期避免数据库超时断开需要留意的是每个数据源都是独立的连接池内存占用是叠加的。你配置了 3 个数据源每个 30 个连接那应用最多可能持有 90 个数据库连接。连接比较多的库比如主库可以给大一点从库或日志库不需要那么大。这个比例要结合数据库本身的 max_connections 上限来规划别光顾着应用性能把数据库压垮了。5.2 慢 SQL 与连接池监控多数据源环境里最怕的不是某个库慢而是不知道是哪条慢 SQL 导致连接池被打满。建议给每个数据源配置独立的慢 SQL 日志和监控指标比如开启 MyBatis-Plus 的慢 SQL 拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }又或者加一个 SQL 性能分析插件把执行时间超过阈值的 SQL 打印出来。这个对定位慢查询非常有效。我上线新系统时一般会在测试环境开 SQL 日志观察每个接口实际路由到了哪个库、执行的 SQL 长什么样确认没问题后再关掉减少日志输出压力。6. 常见问题与排查技巧实录下面是实操中大概率会碰到的几个问题我按“症状 - 原因 - 解决”来梳理。6.1 启动时报 “Failed to configure a DataSource”这个报错在排除了数据库连接配置错误之后最常见的原因是 SpringBoot 自动配置的数据源和自定义数据源冲突了。解决办法是显式排除 DataSourceAutoConfigurationSpringBootApplication(exclude {DataSourceAutoConfiguration.class}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }或者明确指定spring.autoconfigure.excludeorg.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration。这样做的作用是告诉 SpringBoot我自己管理数据源不用你自动装配了。但如果用了我上面给的 Primary DynamicDataSource 方案通常不需要这一步一旦出现这种问题你首先要检查的是 spring.datasource 配置节点和 ConfigurationProperties 前缀是否匹配。6.2 数据源切换不生效一直走默认库这个问题的排查优先级很高。我遇到过的情况有两种第一种切面没生效。检查自定义切面有没有加上 Aspect 和 Component 注解再检查 spring-boot-starter-aop 依赖是否引入。还有一种可能是同类自调用也就是同类里一个方法调用另一个带 DS 注解的方法AOP 不生效。解决办法是把被调用方法单独拆到一个类里或者使用 AopContext.currentProxy() 获得代理对象再调用。第二种事务先行了。如果外层方法加了 TransactionalSpring 在进入你的代码之前就已经拿到主库连接事务内部的数据源切换自然不会生效。这个需要你把 Transactional 拆开让每个数据源的操作分别进入独立事务或者用编程式事务。6.3 报 “Cannot determine target DataSource for lookup key”这个错误信息非常直观路由数据源找不到你要的 key。最常见的几个原因如下DS 注解里的值和 targetDataSources Map 里的 key 不一致。比如注解写了 slave 但 Map 里 put 的是 Slave。动态数据源的异常拦截没配置好比如使用 DS(notExist) 这种明显不存在的 key。targetDataSources 是普通 HashMap有并发修改的问题。不过通常在初始化阶段就 put 完了不会触发。为了稳妥我还是建议用 ConcurrentHashMap。解决方案建立一套常量类把数据源的 key 统一定义比如 DSType.MASTER 和 DSType.SLAVE修改和引用都走常量从根上避免手误。6.4 多数据源下 MyBatis-Plus 分页失效这个遇到的人也不少。MyBatis-Plus 分页需要配置 PaginationInnerInterceptor但在多数据源场景下这个拦截器要拿到当前数据源的数据库类型才能正确生成分页 SQL。如果你把所有数据源都配成了一种数据库比如都是 MySQL那直接写死 DbType.MYSQL 也没问题。但如果你的多数据源涉及不同数据库类型比如 MySQL PostgreSQL就不能用写死的方式。需要实现一个动态获取数据库类型的处理Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); DynamicDatabaseInterceptor dynamicDbInterceptor new DynamicDatabaseInterceptor(); interceptor.addInnerInterceptor(dynamicDbInterceptor); return interceptor; }核心思路是拿到连接后通过连接的元数据获取当前实际数据库类型再动态决定分页方言。这个细节不处理好你切换数据源后分页 SQL 可能带上了错误方言轻则分页不准重则直接把 SQL 发到数据库上执行报错。6.5 连接池泄漏与连接耗尽当连接池出现连接耗尽时系统通常会表现为某个接口突然变慢后续请求大量超时。虽然错误信息会提示连接超时但很多人的第一反应是数据库慢而不是连接池被占满。排查方式也很直接看连接池的 active 数量和数据库侧的 sleep 连接数。HikariCP 自带监控指标可以通过 actuator 暴露出来。我在生产环境就把 jdbc 连接池相关的指标接入了监控系统一旦 active 连接数接近 maximum-pool-size就立刻告警。常见原因往往是代码中的“未关闭连接”。使用 MyBatis-Plus 一般不会出现这个问题因为框架处理得很完善。但如果你手动使用了 JdbcTemplate 或原生 JDBC 操作数据源就必须小心每一个 Connection 的关闭时机。尤其是项目使用动态数据源的时候如果自定义的代码中途抛异常且未进入 finally 清理连接就直接泄漏掉了。关于这点我建议所有手动获取连接的地方都严格按照 try-with-resources 来写。7. 多数据源方案的进阶扩展基础版跑通之后你可以根据业务需求继续往深了扩展。7.1 动态新增数据源运行期注册有些系统要求不用重启应用就能切换新增的数据源比如多租户场景下每个租户一个库。理论上可以在应用运行时调用 DynamicDataSource 暴露的新增方法往 Map 里 put 新的数据源键值对。我做过的一个项目就是用这种方式每接入一个新租户就通过接口把数据源注册进路由表。但这里面有几个坑连接池是重量级资源频繁增删会带来系统开销数据源 key 的管理要配套生命周期租户下线要及时移除对应数据源和连接池。这种方式适合运维能力强的团队建议谨慎使用。7.2 读写分离下的负载均衡如果从库有多个你可以稍微改造一下路由逻辑。在 DataSourceContextHolder 里把同一个逻辑数据源映射到多个物理数据源每次切换时从这几个库中选一个最空闲或随机的。实现原理不复杂就是维护一张配置表在确定路由 key 时做一次负载策略选择。不过要提个醒主从之间存在同步延迟你的业务对数据一致性要求较高时一定不能随便把强一致性的读请求放到从库。常见的做法是强制路由比如刚写入的数据在会话内读主库或者延迟较低的对账业务才允许走从库。7.3 多数据源结合 MyBatis-Plus 多租户插件MyBatis-Plus 的租户插件实现思路是给所有 SQL 自动拼上租户 ID 条件。多数据源加多租户后要注意租户 ID 和租户数据源的映射关系。通常做法是在请求线程拦截器里先根据请求头解析出租户 ID再动态设置数据源 key 和租户上下文。这个时候ThreadLocal 里除了要存数据源 key还要存租户 ID所以在 DataSourceContextHolder 之外再加一个 TenantContextHolder各自管理各自的上下文。这个设计能同时解决“用户要访问哪个库”和“在库里要过滤哪些数据”两个问题。但组合复杂度较高建议在基础版跑通并稳定运行之后再逐步演进。7.4 其他实现方案MyBatis-Plus dynamic-datasource 插件最后再简单补充一下 MyBatis-Plus 的 dynamic-datasource 插件方案毕竟这也是一个主流选择。如果你用的是纯 SpringBoot MyBatis-Plus 技术栈引入dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.6.1/version /dependency然后在 yml 里配置spring: datasource: dynamic: primary: master strict: true datasource: master: url: jdbc:mysql://localhost:3306/ds_master?... username: root password: root123 slave: url: jdbc:mysql://localhost:3306/ds_slave?... username: root password: root123使用起来更简单直接DS(slave) public ListUser listSlaveUsers() { return userMapper.selectList(null); }但我的建议是如果你是为了面试或深入学习一定要先从 AbstractRoutingDataSource 手工实现一遍再去用插件。手工实现能让你彻底理解 AOP 切面、ThreadLocal 上下文、连接池绑定的底层原理这些知识在排查线上问题时会直接变成你的反应速度。直接上手封装的插件确实开发效率高但一旦遇到不符合插件默认行为的问题就会比较被动。8. 写完这段代码我在实际测试里体会到的几件事代码写完之后建议不要急着直接连生产库做验证。一定要在本地起两个 MySQL 实例或者两个 Schema用清晰的测试数据先验证路由逻辑。我这个方案在本地验证的时候发现一个非常容易忽略的事SpringBoot 的懒加载数据源机制。HikariCP 默认是懒加载连接的也就是说即使你配置了错误的数据源连接信息应用启动的时候可能并不会报错要等第一次请求真正去获取数据库连接时才会暴露问题。所以我每次改完数据源配置都会写一个最简单的 Controller 或者用单元测试把每个库的查询都跑一遍。验证顺序也很简单先查主库数据确认返回 master 开头再查从库数据确认返回 slave 开头最后在没有任何 DS 注解的方法里查一次确认默认走了主库兜底。三步验证通过这个数据源配置才算真正稳定。比配置代码本身更重要的是团队约定。动态数据源方案给你了很强的灵活性但是代价是代码的可读性下降。一个方法到底访问哪个库需要看类上注解、方法注解、切面逻辑、事务边界才能判断。因此我建议使用这个方案时在项目 README 里专门写一节“数据源路由约定”把哪些业务模块必须走哪个库固化下来省得后来者读代码时一头雾水。有一次我接手一个同事的项目他的多数据源配置里把主库和从库的连接池参数调成完全一样其实没什么问题但读库有大量统计报表连接需求大主库却是高并发写连接需求反而没那么多。这种细微的配置差异如果不根据实际业务流量调整等到大促流量一来就会出问题。所以连接池参数的调整一定要结合真实的流量模型来迭代而不是一劳永逸。

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

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

免费获取报价