
做Java后端这些年Spring Boot的多数据源问题几乎成了每个中型以上项目都会遇到的必修课。我刚带项目那会儿也天真过觉得一个数据库搞定一切等业务真起来之后订单、用户、日志、报表全挤在一个库里慢查询、锁竞争、磁盘IO飙高这些问题一个个冒出来。这时候最朴素也最有效的解法就是拆库不同的业务模块用各自的数据库再在应用层按需连接和切换数据源。这篇文章我就以Spring Boot为基础把多数据源的连接配置、切换机制、事务边界这些关键点完整捋一遍并且把我在实际项目中踩过的坑和排查思路一并写出来。不管你是刚接触多数据源的新人还是被线上串库问题折磨过的老手我相信都能从这里找到对应的解法。1. 为什么需要多数据源先搞清楚背景再做技术选型1.1 多数据源的典型业务场景多数据源并不是为了技术炫技它背后对应着非常具体的业务痛点。我见过不少团队一开始就是一个库搞定所有业务等数据量冲上来之后单库的毛病开始集中暴露订单表几千行数据的统计查询能把业务TPS拖下来日志表几个月不清理能吃掉几个G的磁盘读写都在同一个库上还会互相争抢连接资源。这时候最自然的解法就是把不同职责的数据拆开各自独立部署、独立扩展而应用层就需要具备同时连接多个数据源、按需切换的能力。具体来说我整理了几类最常见的多数据源场景。第一类是读写分离主库承担写入从库分担查询查询量一大就需要把读请求动态切到从库这是多数据源在大流量项目里最常见的落地形态。第二类是多业务模块分离用户、订单、商品、日志等各自独立成库防止一个模块的慢查询拖垮其他模块的响应也方便后续单独迁移和扩容。第三类是多租户架构不同租户的数据放在不同库里租户请求进来时根据租户标识动态路由。第四类是统计报表与分析场景业务库和报表库分离重型的聚合分析SQL只打到分析库上避免影响在线交易。这些场景的本质诉求是一致的应用需要同时管理多个DataSource并且在一次请求甚至一次方法调用中根据当前上下文动态决定“这一条SQL应该走哪个库”。而Spring Boot默认配置只支持一个DataSource所以多数据源项目的核心难点就从“怎么连”变成了“怎么切”。1.2 多数据源方案选型的核心取舍网上关于Spring Boot多数据源的方案五花八门主流的大致可以分成三条路线。第一条最粗暴写多个配置类每个配置类里各自构建DataSource、SqlSessionFactory再分别注入到不同的Mapper包下。这个方案代码量大每接一个新的业务模块都要重复一套配置而且多个SqlSessionFactory之间的边界一旦没理清很容易出现Mapper拿到了A库的会话、执行的却是B库连接这种混乱。如果项目里只有两个数据源且切换逻辑固定这个方案还能凑合用数据源一多就不行了。第二条就是我这篇文章的落地主线动态数据源方案。核心思路是定义一个实现了AbstractRoutingDataSource的“路由型DataSource”内部维护一张key, DataSource的映射表对外仍然是一个普通的DataSource。业务代码执行SQL时都从路由型DataSource获取连接而路由型DataSource会根据当前线程上下文里的key决定真正返回哪个数据源的连接。切换逻辑通过注解和AOP切面收口业务代码层面不需要感知数据源的存在这也是目前社区最广泛应用的多数据源落地方式。第三条是直接引入第三方框架比如dynamic-datasource-spring-boot-starter或者ShardingSphere。说实在话如果项目里已经有这类组件或者刚需读写分离、分库分表直接引入成熟框架比手写要省心很多框架内部已经帮你处理了事务切换、连接池监控等一堆细节。但自己手写一遍仍然很有价值因为只有亲手把路由机制、ThreadLocal、AOP切面这些部件组装起来你才能真正理解数据源切换的底层逻辑以后不管换什么框架都能快速上手线上出问题也能从原理层面去排查而不是只会对着文档抄配置。1.3 这套方案适合谁如果你正在负责一个Spring Boot项目并且已经面临“多个数据库、按需切换”的诉求这篇内容就是为你准备的。又或者你之前一直只用单个数据源对动态数据源的概念只是听说过但没真正在代码里落地过那这篇也可以当一份从零上手的实操笔记。文章里不会只丢一个能跑的示例我还会把背后的路由机制、ThreadLocal的角色、事务边界、连接池参数这些容易踩坑的细节一并拆开讲。毕竟多数据源本身不难难的是出了问题之后你能不能快速定位到是哪一层惹的祸。2. 核心原理拆解动态数据源切换到底在切换什么2.1 从DataSource到AbstractRoutingDataSource先补一个很容易被忽略的基础认知在Spring的数据库访问体系里Service层和Mapper层其实从不直接持有物理数据库连接。它们拿到的DataSource本质上是一个“连接工厂”每次需要操作数据库时连接池从这个工厂里借出一个Connection用完再归还。Spring Boot自动配置DataSource时只会从配置文件里读一套jdbc参数生成一个单一实例所有Mapper都共享这一个连接工厂。多数据源动态切换的突破口正好就在这里。我们绕开Spring Boot默认绑定的单一DataSource自己定义一个新的“路由型DataSource”。它本身也是一个DataSource但它不直接创建物理连接而是持有其他多个真实数据源的引用。当有人向它请求连接时它先根据当前线程上下文拿到一个key再从内部的映射表里取出对应的真实DataSource由这个真实DataSource去创建连接。谁创建连接、对调用方来说是完全透明的调用方只知道自己从“那个路由型DataSource”拿到了一个能用的连接。这个设计在Spring里早就预留了抽象支持就是AbstractRoutingDataSource。它最核心的工作有两件事第一通过构造时传入的targetDataSources把我们在配置里声明的多个真实数据源放到一个Map里第二每次getConnection()时调用抽象方法determineCurrentLookupKey()来获取当前应该使用哪个key再根据key定位到真实数据源。我们要做的核心实现就是继承这个抽象类在determineCurrentLookupKey()里返回我们自定义的key。理解这个机制之后你会发现多数据源切换本质上不是“切换连接”而是“切换获取连接的路径”。连接可以复用池子里的物理连接数量也无需翻倍只要在方法调用时正确地选择从哪个池里“借”连接就行。这个认知对后面理解事务失效的问题特别重要。2.2 ThreadLocal切换数据源的“记忆载体”那么“当前应该使用哪个key”这个状态应该保存在哪里Java Web应用里同一个线程往往贯穿一次请求的大部分调用链而且大多数数据库访问操作都是同步的所以用ThreadLocal来保存当前数据源key是最自然、开销也最小的选择。ThreadLocal的语义就是“线程私有变量”在这个线程里设置了一个值后续同一线程内任何地方都能读到线程结束之后必须清理掉否则线程池复用线程时下一次请求就可能读到上一次残留的key造成数据源串库。ThreadLocal写起来很轻松一个set、一个get就完事但真正考验人的是清理时机。我见过不少刚开始做多数据源的同事把set写上了remove却漏掉了结果测试环境短暂正常压测一到就出现用户数据错乱一会儿查到A库一会儿查到B库。查到最后才发现是线程池复用线程时ThreadLocal没有清干净属于特别典型却又不好定位的生产事故。所以在设计阶段就要把“谁负责清理”定下来支持用AOP切面统一在环绕通知的finally里清理避免让业务代码自己做清理。还有一个细节值得提Web请求如果中间有异步操作比如Async方法、MQ消费者、CompletableFuture子线程ThreadLocal的值是不会自动传递到子线程的因为子线程和父线程并不共享ThreadLocal。如果业务确实需要跨线程保持数据源路由信息得配合TransmittableThreadLocal或者手动把key传递过去。这个属于进阶问题但提前知道比踩坑之后才知道要好得多。2.3 连接池参数在多数据源场景下的特殊考量多数据源不是简单配置多份jdbc地址就完事连接池层面的参数也需要单独思考。不同数据源的并发压力、响应时间要求可能有天壤之别比如订单库写入峰值高需要更大的maximum-pool-size报表库可能一分钟才几个查询连接池开太大反而浪费宝贵的数据库连接数。我强烈建议每个独立数据源单独配置连接池参数不要图省事一套参数复制到底。另外一个容易被忽视的点是多数据源下的连接监控。一旦出现连接泄漏或者慢查询你首先得判断是哪个数据源的池如果每个连接池都只是一个默认名字看日志只会看到一堆HikariPool-1、HikariPool-2根本对不上号。HikariCP支持通过pool-name自定义连接池名称Druid也有类似配置给每个数据源一个可识别的名字是成本最低的排障前置手段。还要提一下MySQL连接的超时问题。MySQL服务端有wait_timeout默认可能只有几小时连接空闲超过这个时间就会被服务端主动断开。如果客户端连接池的max-lifetime设置得比wait_timeout还大那池里的连接可能已经被服务端断开客户端却不知道下一次拿这条连接做查询时会报连接失效。多数据源环境下一个库调整了wait_timeout另一个库没有这种问题更容易被稀里糊涂地甩到“网络不稳定”上。3. 实操落地从零搭建一个可用的多数据源切换模块3.1 依赖引入与基础配置先说下我这次演示的环境Spring Boot 2.7.xMyBatis-Plus 3.5.x连接池用HikariCP。选HikariCP的原因很直接Spring Boot默认就集成了它性能好、配置简洁不需要额外引入第三方池用起来最省事。如果你项目中已经在用Druid后面代码只要把DataSource的构建方式换成DruidDataSource路由逻辑完全不用动。pom.xml里的关键依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.aspectj/groupId artifactIdaspectjweaver/artifactId /dependency注意一点使用多数据源之后不能再依赖Spring Boot的自动DataSource配置需要在启动类上把DataSourceAutoConfiguration排除掉否则框架启动时检测到多份jdbc配置会报错或者只默默生成一个默认数据源导致后面的路由配置完全失效。启动类上的写法是SpringBootApplication(exclude {DataSourceAutoConfiguration.class})这个排除动作建议在所有多数据源项目里当成标配别再问“为什么我配置了多数据源没生效”八成都是自动配置还在从中作梗。3.2 数据源配置类与动态路由实现接下来我们在application.yml里维护两个数据源这里以业务库和报表库为例示意一下完整的配置结构spring: datasource: business: jdbc-url: jdbc:mysql://192.168.1.10:3306/business_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: BusinessHikariPool maximum-pool-size: 20 minimum-idle: 5 report: jdbc-url: jdbc:mysql://192.168.1.20:3306/report_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: ReportHikariPool maximum-pool-size: 5 minimum-idle: 1这里有个细节值得单独说明Spring Boot 2.x的多数据源配置推荐使用jdbc-url而不是url。Spring Boot 2.x对url属性的自动处理方式在多数据源场景下容易出问题改成jdbc-url后就避开了这些暗坑。另外MySQL连接建议显式加上useSSLfalse和serverTimezone能避免很大一部分部署环境下的连接报错和时区错乱问题。然后编写DataSource配置类把两个真实数据源和路由型DataSource都注册到Spring容器里Configuration public class DataSourceConfig { Bean ConfigurationProperties(spring.datasource.business) public DataSource businessDataSource() { return DataSourceBuilder.create().build(); } Bean ConfigurationProperties(spring.datasource.report) public DataSource reportDataSource() { return DataSourceBuilder.create().build(); } Bean Primary public DataSource routingDataSource( Qualifier(businessDataSource) DataSource business, Qualifier(reportDataSource) DataSource report) { MapObject, Object targetDataSources new HashMap(); targetDataSources.put(business, business); targetDataSources.put(report, report); DynamicDataSource routing new DynamicDataSource(); routing.setDefaultTargetDataSource(business); routing.setTargetDataSources(targetDataSources); return routing; } }注意Primary注解一定不能少。因为项目里其他组件事务管理器、MyBatis的SqlSessionFactory等默认会去拿容器中唯一的DataSource如果存在多个DataSource实现而不标注主数据源Spring会在注入时直接抛出“expected single matching bean but found 2”之类的异常。路由型DataSource作为门面理应被标记为主数据源所有默认的数据库访问入口都从它走。DynamicDataSource的核心实现其实只有几行继承AbstractRoutingDataSource并重写determineCurrentLookupKey()即可public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceKey(); } }这段代码的含义就是每次有人要从路由数据源获取连接时都去ThreadLocal里看当前key是什么有key就按key找对应的真实数据源没有key就用默认数据源兜底。源码虽然简单但它是整个切换机制的“心脏”。3.3 ThreadLocal的载体类与AOP切面ThreadLocal载体建议单独写一个类方便统一管理也方便以后扩展比如加默认值、加日志。我习惯这样写public class DynamicDataSourceContextHolder { private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); public static void setDataSourceKey(String key) { CONTEXT_HOLDER.set(key); } public static String getDataSourceKey() { return CONTEXT_HOLDER.get(); } public static void clear() { CONTEXT_HOLDER.remove(); } }载体有了之后还要确定“什么时候设置、什么时候清除”。我采用的方案是自定义一个DataSource注解再配合Spring AOP的环绕通知统一处理Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DataSource { String value() default business; }切面逻辑这样写Aspect Component public class DataSourceAspect { Around(annotation(dataSource)) public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable { DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value()); try { return point.proceed(); } finally { DynamicDataSourceContextHolder.clear(); } } }代码虽短但有三个细节一定要说清楚。第一切面切入的是注解标注的方法方法执行期间ThreadLocal里始终保存着目标数据源key业务代码执行SQL时路由刚好能读到第二finally里必须调用clear这是防止ThreadLocal串库的生命线第三方法内如果嵌套调用另一个标注了其他数据源的方法由于Spring AOP是代理实现this方式的内部调用不会触发切面这个坑很隐蔽我自己的习惯是切换粒度控制在Service方法这一层每个方法声明自己走哪个数据源避免类内部互相调用时出现“切面没生效”的困惑。如果想让注解支持类级别默认值、方法级别覆盖可以在切面里多写一点优先级逻辑。比如类上标注了DataSource(report)类内某个方法又标注了DataSource(business)就以后者为准。这个扩展在有多租户场景的项目里很实用但我建议刚开始做时先保持简单等业务确实需要再往上面叠复杂度。3.4 MyBatis-Plus多数据源接入如果你的项目刚好用了MyBatis-Plus多数据源场景下其实还有一条捷径直接用官方dynamic-datasource-spring-boot-starter通过DS注解进行切换。它底层的思路和我前面手写的基本一致也是借助AbstractRoutingDataSource和AOP实现的只是把更多细节封装得更好尤其是在事务内切换的兼容处理上做了很多优化。如果项目工期紧或者不想维护自研的动态数据源代码我真心建议直接用这个成熟方案省下的时间去处理真正的业务问题更划算。但如果选择手写方案和MyBatis-Plus整合时有一个架构层面的选择要做。最简单的做法是只让路由型DataSource作为primary数据源SqlSessionFactory通过它创建这样Mapper代理对象实际执行SQL时每次都从路由DataSource中获取连接而路由DataSource会按当前ThreadLocal的key返回对应的真实连接。也就是说只要把路由型DataSource作为唯一入口MyBatis-Plus默认的SqlSessionFactory配置不需要额外改动所有Mapper自动具备多数据源切换能力。如果你非要多配置几个SqlSessionFactory那就要格外小心事务管理器、Mapper扫描范围的绑定关系否则很容易出现“Mapper拿到的SqlSession绑定到A库实际Connection却是B库”的混乱状态。我在几个项目里踩过几次这种坑之后现在的建议非常明确优先使用“单SqlSessionFactory 路由DataSource”的架构理解成本低、排查成本低、坑也最少。只有当你希望不同的数据源使用不同的MyBatis插件或者完全独立的事务控制时才值得引入多个SqlSessionFactory。3.5 切换效果验证最后写一个Demo Service和Controller实际验证切换是否生效。为了便于观察我故意让两个库里的表结构保持相同但表中数据内容不同这样查询结果能直接告诉我们“当前连的到底是哪个库”。Service public class DemoService { Autowired private DemoMapper demoMapper; DataSource(business) public ListDemoEntity queryFromBusiness() { return demoMapper.selectList(null); } DataSource(report) public ListDemoEntity queryFromReport() { return demoMapper.selectList(null); } public ListDemoEntity queryFromDefault() { return demoMapper.selectList(null); } }Controller里暴露三个接口分别调用这三个方法。测试时先访问默认库接口确认走的是business库再访问注解标注为report的接口如果能在结果里看到报表库的数据内容说明路由已生效。接着再访问一次默认库接口如果还是business库的数据说明ThreadLocal清理正常没有发生串库。这三个接口验证下来基本可以覆盖多数据源切换的核心链路默认路由、显式切换、切换后还原。我自己在项目里还会再补一个验证点在同一个Service方法里先查business库再查report库看两次查询是否互不干扰。这一步能提前暴露ThreadLocal清理或者连接池交叉的问题比上线之后再排查要省心得多。4. 项目中的真实坑事务、连接与排查实录4.1 事务边界导致的数据源切换失效这里是多数据源场景下最大的坑没有之一。数据源切换在事务方法里可能完全失效原因要从Spring事务的实现机制说起。Spring的事务管理器在方法启动时会从DataSource中获取一个Connection并把这个Connection绑定到当前线程的事务上下文里整个事务期间都复用这一条物理连接。如果事务已经开启再往同一线程的ThreadLocal里切换数据源key事务内部的Connection并不会跟着变化它还是最初获取的那个库的连接。这个问题的根源在于事务获取连接发生在“数据源切换之前”而不是“每次SQL执行时”。所以哪怕你的AOP切面正确设置了ThreadLocal只要事务拦截器的执行顺序优先于数据源切换切面或者方法内部已经进入了事务边界后面的动态路由就不会再起作用。我在项目里遇到过不少类似事故最典型的就是一个Service方法上标注了Transactional同时又标注了DataSource(report)看起来两个注解都加了实际查询却全部走的是默认库。我的实战建议是数据源切换方法不要和外层事务混在一起。让标注了数据源注解的方法保持非事务状态每个数据源的操作各自独立事务。如果业务确实需要跨库事务不要指望靠动态切换实现应该引入分布式事务中间件比如Seata的AT模式或者用消息队列做最终一致性。读写分离、报表查询这类场景基本不需要强事务这个约束在大多数业务下都是可以接受的。关于切面执行顺序如果你确实控制不了方法层级也可以通过Order注解让数据源切面的优先级高于事务切面让ThreadLocal先设置好事务再从路由DataSource上获取连接。但这种方法只能解决“切面顺序”层面的问题解决不了“事务已经持有连接之后再做切换”的彻底失效所以最终还是要回归到“事务边界与切换边界保持一致”这个设计原则上。4.2 连接持有与归还多数据源环境下的排查要点第二个高频问题就是连接池耗尽。多数据源环境下如果某个数据源的连接被长期占用不释放排查时最大的障碍是很难第一时间定位“是哪个池出了问题”。所以我在前面反复强调每个连接池都要起一个可辨识的pool-name比如BusinessHikariPool、ReportHikariPool。一旦出现等待连接超时日志里会直接打出对应的池名排查范围瞬间缩小到一个库。HikariCP在多数据源下有几个参数需要特别留意maximum-pool-size决定每个数据源允许的最大连接数connection-timeout是获取连接的超时时间建议不要设得太大否则连接池被打满时请求会长时间挂起用户侧看到的就是接口无响应idle-timeout是连接空闲后被回收的时间max-lifetime是连接最大存活时间它必须小于MySQL服务端的wait_timeout否则会出现服务端主动断开、客户端还拿着旧连接继续用的问题。我举个真实调参的例子。之前报表库的上游数据量突然涨了将近十倍单次报表查询耗时长而连接池maximum-pool-size却一直停在上线初期的10结果所有连接全被慢查询占满后续请求全部排队等连接接口RT直接飙到几十秒。后来把maximum-pool-size调整到30同时把connection-timeout从30秒收紧到10秒让请求在排队过久时快速失败而不是无限阻塞接口稳定性立刻恢复。这里其实是个很通用的排障直觉遇到接口偶发超时先看连接池等待时间和数据库活跃连接数不要一上来就加机器。4.3 常见问题速查表我把这些年在项目里实际遇到过的、以及同事向我求助过的问题整理成了一张速查表也算给后来者的一个快速检索入口。现象可能原因排查/解决思路切换了注解但实际走默认库AOP切面没生效或注解没被扫描方法内部this调用导致切面不拦截确认切面配置改用外部代理调用控制切换粒度到Service方法层数据串库A接口查到了B库数据请求结束后ThreadLocal没remove线程池复用时残留key在切面finally里统一清理增加拦截器兜底清理启动报DataSource找不到唯一bean容器中有多个DataSource实现但没标注主数据源在路由DataSource上标注Primary事务方法内切换不生效事务提前获取Connection后续切换key不影响已有事务连接拆分事务边界跨库场景改用分布式事务方案连接池耗尽接口RT飙升单库连接数不足或连接泄漏打开连接池日志定位pool-name调整maximum-pool-size和connection-timeoutMySQL连接偶发失效SSL、时区参数没配好或max-lifetime大于wait_timeout连接串加useSSLfalse与serverTimezone调整max-lifetime小于wait_timeout数据源key在异步线程里丢失ThreadLocal不跨线程传递使用TransmittableThreadLocal或手动传递key这张表里的每一行背后都是一个真实的线上事故或者至少是测试阶段的血泪教训。尤其是第二条串库问题我建议所有多数据源项目在框架层面增加一个兜底清理机制比如通过OncePerRequestFilter在请求结束时强制清除当前线程的ThreadLocal哪怕某个切面漏了清理线程复用时也不会把脏数据带到下一个请求。4.4 进阶方向读写分离、分布式事务与监控多数据源切换这套能力一旦建立起来扩展方向其实非常清晰。第一个方向是读写分离本质就是根据SQL类型决定数据源key对select请求路由到从库对insert、update、delete请求路由到主库。实现上可以在AOP切面里通过方法名、注解或者MyBatis的拦截器判断SQL类型也可以引入现成的中间件。但要注意主从延迟带来的数据一致性问题我个人的原则是一致性要求高的读接口尽量走主库纯查询类、对实时性不敏感的场景才走从库核心是别让“读写分离”牺牲掉业务正确性。第二个方向是分布式事务。多数据源切换之后最难处理的问题就是跨库写入。动态切换在单个事务内无法真正同时操作两个库所以跨库强一致不能靠把多个库的操作塞进一个大事务来解决。常见的两种取舍是引入Seata这类分布式事务中间件做AT模式或者拆解业务流程用消息队列加本地消息表做最终一致性。这个话题展开讲又是一篇长文但核心提醒只有一句不要为了省事把跨库强一致写成单库大事务局部锁和长事务对高并发业务的伤害往往比拆成分布式事务还要大。第三个方向是监控。多数据源方案要长期稳定运行监控必须覆盖到每一个独立数据源。无论是HikariCP的Metrics、Druid的监控页面还是接入Prometheus加Grafana做指标大盘都要确保每个数据源的连接数、活跃连接数、等待耗时能被独立看到。我曾有一次在生产环境靠监控告警提前发现某个从库磁盘接近满了在业务和性能受损之前就完成了数据迁移。这种前置投入是真的能省下半夜紧急处理的代价。我在多数据源改造项目里经常提醒团队一句话多数据源从来不是一个纯框架层面的话题它背后是数据库架构演进、事务一致性取舍、监控体系建设等一整套工程问题。代码怎么写只是表象你能不能在出问题时快速定位是哪个库、哪个池、哪个连接在捣乱才是真正的核心竞争力。说实话多数据源切换这套东西我在不同项目里已经完整撸过三遍从最早写多个SqlSessionFactory手动切到后来用AbstractRoutingDataSource加AOP再到现在项目里逐步引入更成熟的框架每一次都比上一遍更清晰。我现在设计数据源方案时固定会先把三件事想清楚连接池参数是否按数据源独立、“切换key”是否只存在于方法边界、事务边界是否和数据源切换保持一致。把这三件事想清楚后面遇到问题基本都能用最朴素的方式定位出来不至于排查到深夜。这也是这篇内容最想传递给你的东西——多数据源的难点从来不在写代码而在于理解它的边界在哪里。