先说结论:如果你的项目里用了Mybatis-Plus,又把Druid当连接池,那多数据源这件事其实已经解决了一大半。真正麻烦的从来不是“配两个数据源”,而是配完之后事务怎么管、Mapper怎么扫、SQL路由怎么切得干净利落。这篇就围绕“Mybatis-Plus搭建Druid多数据源配置”这个话题,把设计思路、核心配置、动态路由原理、事务坑点、排查技巧一次说透,顺便把我自己实际踩过的几个坑也交代清楚。
1. 整体设计思路与数据源方案选型
1.1 什么场景才会用到多数据源
先别急着抄配置,得先想清楚你到底需不需要多数据源。我见过不少项目,其实只有一个库,但为了“以后可能要分库”,提前把多数据源的架子搭上了,结果每次上线都要多排查一层问题,纯粹给自己加戏。
真正需要多数据源的场景无非这么几类:读写分离(主库写、从库读)、多业务库并存(比如订单库和用户库分开)、数据迁移期间新旧库并行、以及报表查询走独立库不影响线上业务。这几类场景对数据源切换的诉求不太一样,读写分离要求按方法或按SQL类型自动路由,多业务库则更看重手动指定数据源的能力。
如果你确认属于其中一类,才有必要继续往下看。多数据源不是越早引入越好,而是越需要才越值得引入。我建议按“业务需求倒推方案”的方式来做选型,而不是因为某个框架支持多数据源就硬上。
1.2 为什么选Mybatis-Plus + Druid这套组合
市面上的选择其实很多:Spring自带AbstractRoutingDataSource可以做动态数据源;ShardingSphere-JDBC也可以管理多数据源;还有专门的dynamic-datasource-spring-boot-starter,配合Mybatis-Plus和Druid使用非常顺。实际上很多团队在用的就是“Mybatis-Plus + Druid + dynamic-datasource”这三件套。
我个人的理解是,Mybatis-Plus的价值不只是CRUD简化,它的分页插件、Mapper扫描机制、条件构造器在单数据源下很好用,但一旦涉及多数据源,它对“每个数据源都要有独立的SqlSessionFactory”这件事是有要求的。而Druid作为连接池,胜在监控能力和参数可控性,特别是数据源多的时候,每个连接池的活跃连接数、慢SQL、瓶颈一目了然。
动态数据源的核心还是AbstractRoutingDataSource,它在Spring抽象层面解决“从多个DataSource中按Key选择一个”的问题。只不过自己手写这套的话,还要处理切面、注解、线程上下文,封装成本不低,所以成熟项目里一般直接用封装好的starter,至少代码层面省心很多。
1.3 方案对比和架构取舍
如果你还在纠结到底用哪种方案,我给你一个参考维度:先看你们团队对“黑盒”的容忍度。用starter方案,基本就是配置加注解,开箱即用,出问题也好查(它本身有日志和拦截器)。用手写方案,灵活性和可控性更高,但后续维护的人必须非常清楚路由逻辑是怎么实现的。
我当时选型的逻辑很简单:项目已经用了Mybatis-Plus和Druid,多数据源需求属于“业务库隔离”而非“极端高并发读写分离”,那么直接用dynamic-datasource这套最省事。它内部基于AbstractRoutingDataSource实现,同时对Mybatis-Plus做了适配,还额外支持了@DS注解,可以精细到方法级指定数据源。相比自己搓一个AOP切面,可靠性和代码量都强太多。
有一点要注意:ShardingSphere-JDBC也支持多数据源,但如果你的项目只是单纯多库,没有分库分表需求,引入它会带来额外的分片引擎概念,学习成本和排查成本都上升。杀鸡用牛刀,没必要。
2. 核心细节解析与前置配置
2.1 依赖引入版本与兼容性
多数据源配置最怕的就是依赖冲突,尤其是Mybatis-Plus和动态数据源之间的版本联动。我的经验是,Mybatis-Plus的版本尽量和dynamic-datasource的版本保持一个大版本区间内,不要用太老的主力版本去配最新的starter,也不要一味追新。
这里给一份我在某公司项目里实际用过的依赖组合(基于Spring Boot 2.7.x):
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.6.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency>有一点必须说清楚:如果你用了dynamic-datasource-spring-boot-starter,它内部会自己去创建数据源,你就不需要再手写DataSource的@Bean配置了,否则会冲突。同理,Druid的spring-boot-starter在这里的作用主要是提供连接池参数和监控能力,但如果你发现动态数据源和Druid的自动配置“打架”,可以只保留动态数据源里的Druid配置项,去掉druid-spring-boot-starter单独依赖。
2.2 主从库配置文件的编写思路
配置文件是多数据源里最直观的部分。我习惯在application.yml里按“主库、从库、业务库”三层结构组织。每个数据源下有哪些配置项,直接决定连接池行为。下面是一份典型的配置模板:
spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://127.0.0.1:3306/master_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 validation-query: SELECT 1 test-while-idle: true time-between-eviction-runs-millis: 60000 slave: url: jdbc:mysql://127.0.0.1:3306/slave_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 3 min-idle: 3 max-active: 10 max-wait: 60000 validation-query: SELECT 1 test-while-idle: true time-between-eviction-runs-millis: 60000primary: master表示默认数据源是master。strict: false表示找不到指定数据源时允许降级到默认数据源;如果是true,找不到就报错。生产环境我建议设成false,开发阶段可以临时开true,能更早暴露配置问题。
Druid参数这块别光抄,要理解每个参数的意义。initial-size是启动时初始化几个连接,5个足够;max-active是连接池上限,20是常规线上配置,如果你预估并发还更高可以调到50,但需要结合数据库的max_connections来看,别把连接池配得比数据库上限还大。validation-query和test-while-idle是防止数据库把空闲连接杀掉,生产环境基本必配。
2.3 Druid监控与防火墙配置
连接池的价值不只是连接复用,Druid的监控功能在排查多数据源问题时很关键。你需要在配置里单独加StatViewServlet和WebStatFilter,这样就能在浏览器里看到每个数据源的连接创建数、活跃数、SQL执行统计。多数据源环境里,这个监控面板比日志更直观,能一眼看出SQL到底是打到了哪个库上。
spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin123 allow: 127.0.0.1 web-stat-filter: enabled: true url-pattern: /* exclusions: /druid/*,*.js,*.gif,*.jpg,*.png,*.css,*.ico filter: stat: enabled: true slow-sql-millis: 1000 log-slow-sql: true wall: enabled: true config: multi-statement-allow: true几点心得:allow这里建议只配内网IP,别开放远程访问,否则等于把数据库前端暴露出去。slow-sql-millis设为1000毫秒是个比较合理的起点,多数据源场景下如果某条SQL走了错误的库,往往会出现偶发慢SQL,这个参数能帮你快速捞到嫌疑SQL。wall防火墙默认开启,如果你项目里用了特殊SQL,可能被拦截,遇到再说,一般不用动。
3. 动态路由原理与重头戏实现
3.1 AbstractRoutingDataSource到底做了什么
想要把多数据源用得明白,还是得回到Spring的AbstractRoutingDataSource这个抽象类。它本质上是一个DataSource的门面,内部维护了一个Map<Object, DataSource>,每个DataSource对应一个key。每次getConnection的时候,它会调用determineCurrentLookupKey()方法拿到当前线程应该使用的key,然后从map里取出真正的DataSource来创建连接。
Spring本身没有实现“当前线程应该用哪个key”的逻辑,它把这个留给开发者。这就给了我们很大的扩展空间:你可以用ThreadLocal存key,也可以用AOP切面去拦截注解,还可以按读/写操作自动决定走主库还是从库。所以你在网上看到的各种“动态数据源”框架,核心思路都一样,区别只在determineCurrentLookupKey()的实现方式。
dynamic-datasource这套starter就是把这一套封装好了,它在determineCurrentLookupKey()里优先读取@DS注解指定的数据源key,如果没有注解,就返回primary指定的默认key。整个路由链路是:请求进来 → AOP拦截到@DS注解 → 把数据源key存入ThreadLocal → 调用Mapper时通过AbstractRoutingDataSource取连接 → 用完清理ThreadLocal。
3.2 核心注解@DS的正确打开方式
@DS注解可以加在Service方法上,也可以加在Mapper接口上。我的建议是加在Service实现类的方法上,因为事务边界通常在Service层,你在这里切换数据源,后续Mapper调用才能吃到正确的数据源。如果直接加在Mapper上其实也能生效,但万一你的Service方法里同时操作了多个库,事务就只能各管各的,非常容易出问题。
常见的写法是:
@Service public class OrderServiceImpl implements OrderService { @DS("master") public void createOrder(Order order) { orderMapper.insert(order); } @DS("slave") public List<Order> queryOrderList() { return orderMapper.selectList(null); } }这里有个值得注意的点:如果你在一个Service方法里先调用了@DS("slave")的另一个Service方法,再回到当前方法操作master,那么数据源的切换是以方法调用栈里的最后一次设置为准的。直白点说,就是内部方法如果覆盖了数据源key,外部方法后续的SQL路由也会受影响。遇到这种情况,要么避免在Service之间交叉调用,要么内部方法调完把key还原。
3.3 手写一个轻量动态数据源路由Demo
如果你不想依赖starter,想自己控制路由逻辑,那理解手写路由是很值得的。我用过手写方案,后来因为项目成员结构原因换成了starter,但这个Demo依然值得保留在知识库里,因为排查问题时会用到这些底层理解。
第一步,继承AbstractRoutingDataSource:
public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> 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(); } @Override protected Object determineCurrentLookupKey() { return getDataSourceKey(); } }第二步,用AOP切面拦截注解:
@Aspect @Component public class DataSourceAspect { @Around("@annotation(ds)") public Object switchDataSource(ProceedingJoinPoint joinPoint, DS ds) throws Throwable { try { DynamicDataSource.setDataSourceKey(ds.value()); return joinPoint.proceed(); } finally { DynamicDataSource.clear(); } } }第三步,配置多个DataSource并注入到DynamicDataSource中:
@Configuration public class DataSourceConfig { @Bean public DataSource masterDataSource() { // 构建DruidDataSource } @Bean public DataSource slaveDataSource() { // 构建DruidDataSource } @Bean public DataSource dynamicDataSource() { DynamicDataSource dataSource = new DynamicDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", masterDataSource()); targetDataSources.put("slave", slaveDataSource()); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(masterDataSource()); return dataSource; } }手写方案的最大问题在于:SqlSessionFactory和事务管理器都得手动绑定到这个动态数据源上,否则Mapper走的数据源和事务控制的数据源对不上。一旦你决定手写,就得把DataSource、SqlSessionFactory、TransactionManager这三者的创建全部接管。
我当时的体会是:手写能让你把原理吃得透透的,但生产环境还是用封装好的框架更高效,尤其团队人多的时候,框架约定比个人习惯可靠得多。
3.4 基于Mybatis-Plus的SqlSessionFactory配置
不管用不用starter,你都绕不开一个关键点:Mybatis-Plus的SqlSessionFactory如果只绑定了一个数据源,那Mapper只会操作那一个库。所以当使用手写动态数据源时,一定要把dynamicDataSource注入到MybatisPlusSqlSessionFactory的构造参数里。
@Bean public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dynamicDataSource) throws Exception { MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean(); factoryBean.setDataSource(dynamicDataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:mapper/**/*.xml")); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); interceptor.addInnerInterceptor(pagination); factoryBean.setPlugins(interceptor); return factoryBean.getObject(); }用Mybatis-Plus时,MybatisSqlSessionFactoryBean这个类是必须的,不能直接用原生的SqlSessionFactoryBean,否则Mybatis-Plus的自动填充、分页插件、逻辑删除全都失效。如果你用的是dynamic-datasource starter,它会自动处理SqlSessionFactory绑定多数据源的事,这也是我推荐它的核心原因之一,省心且不会漏配。
4. 实操过程:从零搭建一套可运行的多数据源
4.1 完整项目结构规划
开始写代码之前,先规划好包结构和数据源命名。我见过不少项目数据源命名混乱,master、primary、main混着用,最后切数据源时都不知道该写哪个。推荐统一命名:master、slave、bizdb这种,语义清晰且简短。
项目结构大致是这样的:
src/main/java ├── com.example.demo │ ├── config │ │ └── MybatisPlusConfig.java │ ├── controller │ │ └── UserController.java │ ├── datasource │ │ └── DynamicDataSource.java │ ├── mapper │ │ └── UserMapper.java │ ├── model │ │ └── User.java │ └── service │ ├── UserService.java │ └── impl │ └── UserServiceImpl.java src/main/resources ├── mapper │ └── UserMapper.xml └── application.yml模块划分清晰有个直接好处:排查问题时能快速定位是多数据源配置的问题,还是业务SQL的问题,还是Mapper映射的问题。实际开发中,这三类问题经常混在一起,如果耦合在一块,debug成本很高。
4.2 从零手把手配置一个读写分离Demo
我以“主库负责写入、从库负责查询”的经典读写分离场景为例,带你走一遍完整搭建过程。
第一步,在application.yml里定义两套数据源。这一步上面已经给过模板,就不再重复。唯一提醒的是:driver-class-name务必用com.mysql.cj.jdbc.Driver,这是MySQL 8+ 的驱动类名,旧版的com.mysql.jdbc.Driver在新版本里会报警告。
第二步,配置Mybatis-Plus的常用插件。读写分离场景下分页是高频功能,所以分页拦截器必配:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }这里setMaxLimit(500L)是我比较推荐的设置,防止误操作查出超大数据集把从库压垮。多数据源环境下,从库往往承担着大量查询流量,一旦有人写了个不带条件的全表分页,很容易拖垮整个库。
第三步,定义实体和Mapper:
@Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; }@Mapper public interface UserMapper extends BaseMapper<User> { }第四步,Service层用@DS切换数据源:
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @DS("master") public void addUser(User user) { save(user); } @DS("slave") public List<User> listUsers() { return list(); } }这样addUser会往主库写,listUsers会从从库读。我建议在Controller里加一个测试接口,分别调用两个方法,然后在两个库里造不同的数据,比如主库存了一条“master-user”,从库存了一条“slave-user”,分别访问看返回的数据就能验证路由是否生效。
4.3 多业务库场景的按需路由
读写分离是“同一份业务数据,不同职责的库”,多业务库则是“不同业务模块,完全独立的库”。比如用户表在user库,订单表在order库。这种情况下,基本思路是:每个业务模块的Service方法上明确指定数据源,不依赖默认数据源。
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private UserMapper userMapper; @DS("order") public void createOrder(Order order) { orderMapper.insert(order); } @DS("user") public User getUserById(Long userId) { return userMapper.selectById(userId); } }这样的好处是职责一目了然,坏处是如果一个Service方法里需要同时操作多个库,就会面临事务无法跨库的问题。这一点我在下一节会专门讲,这也是多数据源配置里真正容易让人崩溃的地方。
4.4 参数调优和监控验证
配置完成后,除了功能验证,还建议做一次参数层面的验证。重点看两件事:连接池是否按预期初始化、Druid监控能否区分出不同数据源的连接数据。
连接池参数我一般用下面的基线,具体数值按业务压力调整:
| 参数 | master值 | slave值 | 说明 |
|---|---|---|---|
| initial-size | 5 | 3 | 启动时创建连接数 |
| min-idle | 5 | 3 | 最小空闲连接数 |
| max-active | 20 | 10 | 最大活跃连接数 |
| max-wait | 60000 | 60000 | 获取连接最大等待时间 |
| validation-query | SELECT 1 | SELECT 1 | 连接检查SQL |
| time-between-eviction-runs-millis | 60000 | 60000 | 空闲连接检测间隔 |
生产环境里max-active要根据压测结果调整。很多团队都有过max-active配太大把数据库拖垮的经历,也有配太小导致高峰期连接不够报错的。我的经验是:先按容量预估50%,压测后再慢慢上调,不要一下给满。
监控验证这块,Druid面板上如果看到两个数据源的URL分别出现,且SQL执行数在增长,说明多数据源配置已经生效。如果你发现所有SQL都打在同一个数据源上,优先检查@DS注解有没有被AOP正确拦截到。
5. 常见问题与排查技巧实录
5.1 最典型的几个报错现象
多数据源配置最常见的报错我整理成了一张表,每一类我都碰到过至少一次:
| 现象 | 根本原因 | 解决方向 |
|---|---|---|
| 启动报DataSourceUrl未设置 | dynamic-datasource的配置被Druid自动配置覆盖 | 调整依赖或排除Druid自动配置 |
| 运行时发现SQL走错库 | @DS注解加的位置不对或AOP没生效 | 把注解加到Service实现方法上,并检查切面扫描 |
| Mapper查询报找不到表 | Mybatis-Plus的SqlSessionFactory绑定了单个数据源 | 确保SqlSessionFactory绑定动态数据源 |
| 事务方法内切换数据源失效 | 事务开启时连接已经绑定,@DS无法改变当前事务连接 | 避免事务方法内切换,或使用支持XA的事务方案 |
| 提示Cannot determine target DataSource | strict: true且找不到注解指定的数据源 | 检查数据源名称拼写,或临时改strict为false |
第一类问题非常典型。用了druid-spring-boot-starter再配dynamic-datasource,有时启动会报url not set。这是因为Druid的自动配置尝试创建一个默认数据源,却没有找到spring.datasource.url。解决办法是排除Druid的自动配置类:
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }老实说踩过这个坑之后我才意识到:动态数据源和Druid自动配置在某些版本里兼容性并不完美,“能用”和“顺利”还是有区别的,遇到这种问题不要怀疑自己配错了参数,先排除自动配置冲突。
5.2 事务与多数据源的世纪难题
这是我特别想展开聊的一点。Spring的@Transactional默认只能控制一个数据源的事务,如果你在一个事务方法里先操作了master,又切到slave查询,那么slave的查询是不会参与当前事务的。更麻烦的是,事务开启时连接已经绑定到master,此时@DS注解即使指定了slave,也可能不会生效,因为连接持有者还是master。
网上有很多人说把@Transactional加在多数据源方法上就能搞定,这是不对的。实际生产环境里,真正能保证跨库一致性的方案只有几种:分布式事务中间件(比如Seata)、本地消息表+最终一致性、或者干脆拆分服务,每个服务管自己的库。
我的建议很直接:如果只是读操作需要走从库,就别加@Transactional;如果是写操作且必须保证一致性,就用独立的事务边界,并把多个库的写入拆分到不同的方法里,配合业务补偿。不要试图在单个事务方法里既跨库又保证强一致,那基本是给自己挖坑。
5.3 动态数据源配置踩坑避坑要诀
除了上面说的,我另外总结几条独家心得:
一是@DS注解和@Transactional的优先级关系。当两个注解同时存在时,Spring事务的拦截器会先于数据源切面执行,所以连接池可能先拿到了默认数据源的连接。即使你后来切了数据源,事务管理的连接还是旧的。这个顺序问题很隐蔽,排查时容易绕圈子。
二是一个数据源对应一套Mybatis-Plus配置。如果你有几个库,且每个库的数据库方言或分页规则不一致,需要检查分页插件是否支持多方言识别。dynamic-datasource在大多数场景下能处理,但如果你用手写方案,分页插件只能绑定一个方言,MySQL和Oracle并存时会出问题。
三是连接池泄漏问题。多数据源环境下,如果少了一个关闭连接的逻辑,连接池会被耗尽,表现就是应用卡死、报连接超时。排查这类问题建议开启Druid的remove-abandoned配置:
druid: remove-abandoned: true remove-abandoned-timeout: 180 log-abandoned: true这条配置能帮你自动回收泄漏的连接,同时把泄漏点日志打印出来,定位代码位置很方便。生产环境建议开一阵子,等稳定了再视情况关掉。
5.4 写操作走主库、读操作走从库的细节优化
读写分离场景下,如果你不想在每个读方法上都加@DS,可以考虑用一个切面做自动路由。判断逻辑可以很简单:方法名以get/select/query/list/count开头的走slave,其余走master。这个方案很粗暴,但确实能减少注解遗漏。不过我不太推荐在重要项目里全量依赖方法名约定,毕竟总有人不按规矩起名字。
更稳妥的做法是:默认数据源保持master,只有明确的查询方法才加@DS("slave")。这样即使你漏加了注解,SQL也是打到主库,不会出现查询走到了从库但数据同步存在延迟导致读到旧数据的问题。毕竟主从延迟是读写分离绕不开的坑,读从库意味着默认接受一定的数据延迟。
一点个人感触:读写分离的真正难点不在配置,而在于业务是否允许短暂的读延迟。如果你的业务强一致要求很高,比如刚写完订单立刻要查到订单,那读写分离天然不适用,这种情况必须强制走主库,或者绕过从库查询逻辑。
5.5 注解失效与切面优先级问题
@DS注解失效是另一个高频问题,排查方向主要有三个。第一个是AOP切面没有被Spring管理,注意@Aspect和@Component这两个注解都要有,少了任何一个切面都不起作用。第二个是自调用问题,即同类内部调用不走代理,Spring AOP天然无法拦截。第三个是切面顺序问题,数据源切面一定要保证在事务切面之前生效,可以通过@Order(Ordered.HIGHEST_PRECEDENCE)来提升数据源切面的优先级。
@Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class DataSourceAspect { // ... }如果你用的是dynamic-datasource starter,它内部已经处理了大部分切面逻辑,遇到失效概率会小很多。但还是建议你把切面知识留着,因为这个坑一旦出现,你排查起来会很有方向感。
6. 结语与实操体会
用了这么久的多数据源方案,我个人最深的体会是:配置本身只占20%,剩下80%的精力都花在“理解路由时机、梳理事务边界、排查隐性问题”上。无论是Mybatis-Plus的SqlSessionFactory绑定,还是Druid连接池的参数调优,又或者是@DS注解和事务的相互作用,每一个点背后都有它在Spring生态里的设计逻辑。你越懂底层,配置报错时就越不容易慌。
最后再分享一个我个人的小技巧:多数据源配置完成后,不要急着写业务代码,先做一个“数据源连通性验证”的小接口,把每个数据源都查一遍,再在Druid监控面板上核对两套连接池的URL。这一步看似多余,实际上能把90%的配置错误拦在业务开发之前,省下后面一大段debug时间。项目里的其他成员接手多数据源相关需求时,我也不会直接给文档,而是带着他们把这个验证流程走一遍,比讲十遍配置原理都管用。