基于Druid多数据源动态切换实战:Spring Boot + MyBatis-Plus配置与避坑 2. 多数据源配置到底在解决什么问题很多项目做到中期一定会遇到一个绕不开的需求同一个应用里需要同时连接多个数据库。常见场景比如主库负责读写业务数据从库负责报表查询或者一个系统里既有业务库又有日志库、配置库更常见的是微服务拆分后不同模块的数据散落在不同库实例里但服务层又必须在一个应用内完成聚合查询。我第一次接触多数据源是在一个实际项目里一个后台管理系统核心业务表在A库但权限和审计日志在B库而且B库是另一个团队维护的表结构不能动连接信息也不能并进主库。当时最笨的办法是直接创建两个独立的数据库连接工具类谁要用谁自己new一个Connection。用了一周就出问题了连接池资源互相独立、事务没法跨库、代码里到处是硬编码的数据源选择逻辑改一个连接串要翻遍全项目。后来才下定决心引入统一的多数据源管理方案。如果只是两个库还勉强能忍一旦库的数量到三个以上用传统方式维护成本会变成灾难。每个数据源都要维护独立的连接池配置、独立的会话工厂、独立的事务管理器而且切换逻辑散落在业务代码里根本没法统一做监控和治理。Druid作为国内使用率极高的数据库连接池本身提供了完善的数据源管理能力加上Spring框架的抽象可以很优雅地解决动态切换的问题。这篇文章按照我实际落地的思路来写先讲清楚多数据源的架构设计思路再拆解Druid在其中的角色然后给出完整的配置与切换实现方案最后把我在生产环境里踩过的坑和排查手法一并整理出来。无论你是刚接触多数据源的小白还是已经写过几个Demo但没上过生产的开发者这篇都值得花十分钟读完。3. 以Druid为底座的多数据源整体设计思路3.1 先想清楚是静态多数据源还是动态切换动手写代码之前我建议你先搞清楚自己的真实需求。市面上大部分文章一上来就让你写一个动态切换数据源的AOP但实际很多场景根本用不到动态切换只是需要在不同模块里用不同的数据源而已。静态多数据源系统启动时初始化多个数据源每个数据源绑定固定的mapper或service调用方不需要关心当前用的哪个库。比如用户模块走主库日志模块走日志库。动态切换数据源同一个方法里根据业务参数或上下文信息在运行时决定走哪个库。典型场景是分库分表中间件对接或者多租户系统里根据租户ID路由到不同数据库。如果只是静态多数据源最简单可靠的方式是用多个SqlSessionFactory每个factory绑定自己的数据源和mapper扫描路径。这种方案不需要AOP不需要动态代理也不存在切换失效的问题代码结构非常清晰。真正需要动态切换的时候再引入AbstractRoutingDataSource或者自研切换工具。我发现很多同学的误区在于项目里刚引入MyBatis-Plus或者MyBatis就直接照搬网上的动态切换模板结果业务上根本用不到切换反而因为多数据源事务、动态代理顺序问题把自己坑惨了。所以第一步一定要做好需求判断这能省掉后面80%的麻烦。3.2 动态切换的常见实现路线对比如果确实需要动态切换业界主流有两条路线基于Spring自带的AbstractRoutingDataSource基于MyBatis的插件或拦截器机制在SQL执行前动态更换数据源我用过这两种方案各有优劣。AbstractRoutingDataSource是Spring框架提供的一个抽象类核心思路是把多个数据源注册到一个路由数据源中通过实现determineCurrentLookupKey()方法返回一个key框架根据key自动从目标数据源列表里取出对应的DataSource。这种方案实现简单切换粒度是线程级别的适合大多数场景。MyBatis拦截器方案则是在执行SQL前通过拦截Executor或StatementHandler根据当前线程上下文重新绑定数据库连接。这种方案粒度更细可以做到SQL级别的切换但实现复杂度高且容易和分页插件、多租户插件冲突不建议用来作为通用方案。实际生产环境里90%的动态切换需求用AbstractRoutingDataSource就足够了。我后面给出的方案也是基于这条路线配合Druid连接池管理稳定性和性能都在线。3.3 Druid在方案中的定位与优势Druid不只是一个连接池它自带监控、SQL过滤、防SQL注入、慢SQL统计、Web监控页面等能力。在多数据源场景下Druid的价值主要体现在三点第一连接池隔离。每个数据源独立配置初始连接数、最小空闲、最大活跃数互不干扰。主库压力大时不会把从库的连接池占满。第二监控统一。Druid的StatFilter会统计每一种SQL在每一个数据源上的执行次数、耗时、并发等指标。多数据源系统最担心的就是某个库慢SQL拖垮整体通过Druid监控页面可以快速定位是哪个数据源的问题。第三连接泄漏检测。Druid内置了连接泄漏检测机制可以设置removeAbandoned来强制回收长时间未归还的连接。多数据源切换时最容易出现连接从A库拿到、代码逻辑走一半就return了连接没归还此时这个检测能力非常救命。我在生产环境里不止一次遇到类似问题某个接口偶发性超时排查后发现是切换数据源时连接没正确关闭连接池被占满。开了Druid的removeAbandoned后问题立刻暴露比在代码里加日志高效得多。4. 核心实现从依赖引入到动态切换的完整配置4.1 依赖与基础环境准备先说明我的技术栈基础Spring Boot 2.x MyBatis-Plus Druid MySQL。如果你用的是Spring Boot 3.x或者JDK 17其实整体思路不变只是部分依赖版本需要调整。引入依赖时有一个坑Druid有独立的Spring Boot Starter也有老版的druid-spring-boot-starter两者包名和自动配置逻辑有差异。我建议使用新版Starter同时注意排除Druid自带的Spring Boot 2自动配置类冲突。下面是一个可用的依赖配置dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency如果你的Spring Boot版本较新还需要确认javax.sql.DataSource相关的自动配置是否被干扰。我遇到过Druid自动配置和Spring Boot自带的数据源自动配置冲突导致启动报错的情况解决办法是在启动类上排除DataSourceAutoConfiguration。SpringBootApplication(exclude {DataSourceAutoConfiguration.class}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }4.2 配置文件多数据源参数如何组织我的项目配置文件是这样写的主要是struct清晰方便后期运维调整。需要注意的细节是Druid连接池参数不要所有数据源共用一套比如主库写多读少最大活跃数可以高一点报表库查询量大但并发低可以调大初始化连接数。spring: datasource: druid: master: url: jdbc:mysql://10.0.0.1:3306/master_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 slave: url: jdbc:mysql://10.0.0.2:3306/slave_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: report password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver initial-size: 10 min-idle: 10 max-active: 30 max-wait: 60000这里有个实际经验如果用MyBatis-Plus你会发现它默认也会去读spring.datasource.url如果找不到就会报错。所以上面配置中我刻意没有配置spring.datasource.url而是直接放到spring.datasource.druid.master.url下为的是避免自动配置冲突。如果你需要兼容其他组件也可以保留一层空的spring.datasource.url占位但那是很脏的做法不推荐。此外每个数据源都应该单独配置Druid的监控参数比如slave库慢查询阈值可以调低一些。具体配置如下spring: datasource: druid: master: filters: stat,wall,slf4j connection-properties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis5000 slave: filters: stat,wall,slf4j connection-properties: druid.stat.mergeSqltrue;druid.stat.slowSqlMillis20004.3 数据源配置类把多个DataSource装配给Druid管理配置类要做两件事把YAML参数读进来创建两个DruidDataSource实例再把这两个实例交给后续的路由数据源使用。我用的方式是直接利用Druid的DruidDataSourceBuilder从配置文件对应前缀读取属性。Configuration public class DataSourceConfig { Bean(masterDataSource) ConfigurationProperties(spring.datasource.druid.master) public DataSource dataSourceMaster() { return DruidDataSourceBuilder.create().build(); } Bean(slaveDataSource) ConfigurationProperties(spring.datasource.druid.slave) public DataSource dataSourceSlave() { return DruidDataSourceBuilder.create().build(); } }这里有一点要注意ConfigurationProperties的前缀必须和YAML里的路径完全一致尤其是spring.datasource.druid.master这种层级少一层或目录对不上都会导致Druid参数初始化失败最常见的问题是连接池参数没生效连接数永远是默认的8。4.4 动态路由数据源实现实现一个继承自AbstractRoutingDataSource的类重点在于重写determineCurrentLookupKey方法返回当前线程对应的数据源key。同时我们需要一个线程级别的上下文管理工具来保存和切换当前数据源标识。我的实现是这样的public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }上下文持有器public class DataSourceContextHolder { private static final ThreadLocalString CONTEXT new ThreadLocal(); public static void setDataSource(String dataSource) { CONTEXT.set(dataSource); } public static String getDataSource() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }很多人容易忽略的地方是ThreadLocal用完后必须清理。否则在高并发场景下由于线程池复用线程当前线程上一次请求里设置的数据源key会被下一次请求读到导致串库。这个问题我在生产环境遇到过异常表现为偶发查询结果错乱排查半天最后定位到就是ThreadLocal没清理。所以每次在业务执行完务必在finally块里调用clear。4.5 将动态数据源注册到MyBatis-Plus有了DynamicDataSource还需要把targetDataSources配置进去同时把defaultTargetDataSource指向主库。Configuration public class MybatisPlusConfig { Bean Primary public DataSource dynamicDataSource( Qualifier(masterDataSource) DataSource masterDataSource, Qualifier(slaveDataSource) DataSource slaveDataSource) { DynamicDataSource dynamicDataSource new DynamicDataSource(); MapObject, Object targetDataSources new HashMap(); targetDataSources.put(master, masterDataSource); targetDataSources.put(slave, slaveDataSource); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(masterDataSource); return dynamicDataSource; } }如果你把这段代码直接复制到项目里一定会发现一个细节Primary必须加在动态数据源上否则Spring依赖注入DataSource时会注入单个具体的DruidDataSource路由功能完全失效。这是我见过最多人踩的坑。此外MyBatis-Plus的SqlSessionFactory也需要依赖这个动态数据源。MyBatis-Plus默认会自动注入DataSource类型的Bean如果容器中有多个DataSource且没有Primary就会启动报错。加上Primary后只要你的DataSourceConfig里没有把具体的数据源也标注为Primary一切正常。接下来要配置SqlSessionFactory和SqlSessionTemplate让MyBatis-Plus使用这个路由数据源。MyBatis-Plus默认的自动配置已经能识别Primary数据源所以这一步通常不需要额外配置。但如果你自己创建了SqlSessionFactory务必保证它注入的是dynamicDataSource。Bean public SqlSessionFactory sqlSessionFactory(Qualifier(dynamicDataSource) DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); // 其他Mapper路径、驼峰配置等 return factory.getObject(); }4.6 实现数据源动态切换的两种常用触发方式拥有了底层路由能力后具体怎么触发切换呢我实际用过两种方式分享给你。方式一手动编程式切换。在Service方法里显式调用DataSourceContextHolder.setDataSource(slave)执行完业务后清理。这种方式灵活但侵入性高适合切换点比较少且不固定的场景。public ListReport queryReport() { try { DataSourceContextHolder.setDataSource(slave); // 执行查询 return reportMapper.selectList(...); } finally { DataSourceContextHolder.clear(); } }方式二AOP切面式切换。自定义一个DataSource注解标记在Service或Mapper方法上通过AOP在方法前切设置数据源方法后清理。Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DataSource { String value() default master; }切面实现Aspect Component public class DataSourceAspect { Before(annotation(dataSource)) public void before(JoinPoint point, DataSource dataSource) { DataSourceContextHolder.setDataSource(dataSource.value()); } After(annotation(dataSource)) public void after(JoinPoint point, DataSource dataSource) { DataSourceContextHolder.clear(); } }AOP方式的好处是业务代码无感知在类上或方法上标注注解即可。但要注意切面顺序尤其是和事务切面的先后问题。如果事务切面先于数据源切面执行那么事务管理器在开启事务时已经获取了连接此时再切换数据源往往不生效。解决办法是让数据源切面执行在事务切面之前。Order(Ordered.HIGHEST_PRECEDENCE) Component public class DataSourceAspect { ... }我用的是方式二因为团队里规范是强制在Service方法上标注数据源这样从代码审查看得很清楚不会出现“猜不到这个查询走的哪个库”的情况。4.7 事务与多数据源切换的互补关系多数据源和事务天然存在冲突因为Spring声明式事务默认绑定在单个数据源连接上。跨数据源的事务要么用分布式事务中间件如Seata要么通过编程式事务自己做补偿。这里要澄清一个很多人误解的地方动态切换数据源针对的是“操作不同库的独立事务”而不是“一个事务里同时操作多个库”。如果你在一个事务里先查主库再写从库那么从库的操作不会和主库的操作处于同一个本地事务中任何一步失败都可能导致数据不一致。这是动态切换方案的边界需要项目设计阶段就明确。实际项目中我很少让一个事务方法里做数据源切换。通常的做法是查询类操作走从库不开启事务写操作固定走主库开启事务。查询从库的Service方法上标注DataSource(slave)同时不要加Transactional或者加了也要保证事务管理的DataSource是动态路由数据源并且当前线程设置的数据源已经被切到从库。如果确实需要事务内切换可以考虑放弃动态切换直接在主Service里调用两个不同数据源的独立Mapper各自开启独立事务必要时用最终一致性方案。这个边界一定要想清楚否则上线后数据错乱会非常难排查。5. 实操详解自建注解实现多数据源动态切换5.1 自定义注解与切面的完整代码这里我给出一个可以直接复制改造的完整实现。第一定义注解Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Documented public interface DataSource { String value() default master; }第二定义切面Aspect Component Order(Ordered.HIGHEST_PRECEDENCE) public class DataSourceAspect { private static final Logger logger LoggerFactory.getLogger(DataSourceAspect.class); Before(annotation(dataSource)) public void doBefore(JoinPoint joinPoint, DataSource dataSource) { String dsName dataSource.value(); DataSourceContextHolder.setDataSource(dsName); logger.debug(数据源切换到: {}, dsName); } After(annotation(dataSource)) public void doAfter(JoinPoint joinPoint, DataSource dataSource) { DataSourceContextHolder.clear(); } }第三在业务代码里使用Service public class ReportServiceImpl implements ReportService { Override DataSource(slave) public ListReport queryDailyReport(Date date) { return reportMapper.selectByDate(date); } }5.2 为什么切面要加Order最高优先级Spring的AOP在同一个方法上存在多个切面时会按照Order的顺序执行。数值越小优先级越高HIGHEST_PRECEDENCE就是Integer.MIN_VALUE也就是最先执行。如果开启了事务Spring的TransactionInterceptor也是一个AOP通知。事务拦截器会在执行目标方法前开启事务并获取数据库连接数据源切换切面的执行顺序如果晚于事务切面那么事务已经基于原来的数据源创建了连接即使后面切换了ThreadLocal中的key也已经晚了。因为事务管理器内部缓存了当前的Connection后续数据访问都走这个连接。解决方式就是给切面加Order(Ordered.HIGHEST_PRECEDENCE)确保数据源先切换然后再开启事务。这个顺序坑很多人没注意一旦数据源切不生效优先检查这里。5.3 开启MyBatis-Plus拦截器开关时的冲突处理MyBatis-Plus本身提供了多租户插件、乐观锁插件、分页插件等。如果你写了自己的MyBatis拦截器插件那么当接口标注了数据源注解时注意不要和这些插件的执行顺序产生冲突。比较常见的问题是分页插件在数据源切换前就已经准备了数据库方言对应的分页语句如果你的不同数据源是MySQL和PostgreSQL混合这种分页插件加动态切换的混用会非常危险。好的实践是在切换场景里尽量保证所有数据源使用同一种数据库方言避免分页SQL模板不同导致语法错误。我在某个项目里曾尝试让一个接口同时支持MySQL库和Oracle库在Controller层根据参数切换数据源结果分页插件的方言由默认数据源决定切到Oracle后分页SQL还是MySQL语法直接报错。最后只能关闭分页插件自己根据数据源类型手写分页函数。所以除非弹性设计否则混用数据库类型时需要考虑的复杂度会指数上升。5.4 动态切换失效的排查思路当你按照上面的代码实现后发现切换根本没生效查询还是走的主库怎么排查第一步确认determineCurrentLookupKey返回的key是否在targetDataSources中。打个断点或者在方法里打印日志就能立刻判断。最常见的问题是写配置时key拼写不一致比如YAML里用master代码里targetDataSources.put(Master, ...)大小写不一样就路由失败默认走主库。第二步确认AOP切面有没有生效。如果注解加在Service实现类上而调用方式是通过同一个类内部调用那AOP是失效的因为Spring AOP基于代理内部自身调用不走代理。解决办法是注入自身代理对象或者把交换逻辑拆到另一个Service中调用。这个坑我也踩过很多次最后从“AOP不生效”查到了CGLIB代理机制。第三步确认事务和切面的先后顺序。如果方法上有Transactional切面又没有加Order(HIGHEST_PRECEDENCE)那就先开事务后切库连接已经绑定死了。按照前文修复切面顺序即可。第四步检查Druid数据源本身是否创建成功。多数据源的配置类如果加载顺序有误比如dynamicDataSourceBean在masterDataSourceBean之前创建那么依赖注入时会因为找不到Bean而报错。通常让Spring管理这些Bean时它会自动排序但如果您在静态代码块或者构造方法里引用了DataSource就要小心了。5.5 多数据源配置化封装按租户或按业务线如果系统里不止两个库比如有十个库用注解明指定每一个库的key会很难维护。我在中大型系统里做过的优化是把数据源标识定义成枚举结合配置文件动态加载。public enum DataSourceKey { MASTER(master), SLAVE(slave), REPORT(report), LOG(log); private final String key; DataSourceKey(String key) { this.key key; } public String getKey() { return key; } }数据源配置也是动态的在配置类中循环读取spring.datasource.druid下的每个子节点自动创建DruidDataSource并注册到动态数据源中。这样做的好处是以后扩展新库的时候只需要在配置文件中增加一段代码里增加一个枚举值不用再改配置类。这种动态配置化方法有两点必须注意一是需要保证读取顺序与Bean依赖顺序可以借助PostConstruct或InitializingBean完成二是不同数据源的密码建议使用密文Druid的ConfigTools可以加密数据库密码然后在配置中使用加密后的值。6. 核心装饰链路详解Druid监控下的多数据源状态6.1 开启Druid监控页面并观察各数据源连接状态Druid自带的Web监控页面是排查多数据源问题的利器。在配置类中注册一个StatViewServlet和WebStatFilter就能在浏览器里访问监控面板。Configuration public class DruidMonitorConfig { Bean public ServletRegistrationBeanStatViewServlet statViewServlet() { ServletRegistrationBeanStatViewServlet registration new ServletRegistrationBean(); registration.setServlet(new StatViewServlet()); registration.addUrlMappings(/druid/*); // 可以设置登录用户名密码 registration.addInitParameter(loginUsername, admin); registration.addInitParameter(loginPassword, admin123); return registration; } Bean public FilterRegistrationBeanWebStatFilter webStatFilter() { FilterRegistrationBeanWebStatFilter registration new FilterRegistrationBean(); registration.setFilter(new WebStatFilter()); registration.addUrlPatterns(/*); registration.addInitParameter(exclusions, *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*); return registration; } }打开/druid页面后可以在数据源列表看到每个数据源的活动连接数、池中连接数、活跃连接数、错误连接数等。多数据源场景下数据源列表里会出现多个数据源条目方便比对。我经常通过这个页面来判断某个从库连接池是否被异常借用如果活跃连接数长时间居高不下基本可以判定连接泄漏或者慢SQL阻塞。6.2 通过监控数据判断数据源切换是否正确如果你想知道某个接口到底查的是哪个库Druid监控里的“SQL监控”标签页能直接给出答案。每个SQL都有对应的数据源名称Druid会显示DataSource类名或URL你可以根据SQL的执行次数和时间来判断它是落在了主库还是从库。我在有一次排查Rect那个偶发超时问题时就是通过Druid监控看到本应走从库的报表SQL出现在主库的数据源SQL列表中。回溯代码后发现问题出在AOP切面没生效Service里用this调用另外一个方法导致代理失效。如果没有Druid监控这种问题靠日志会非常费劲。6.3 Druid连接池参数调优建议多数据源场景下连接池参数的设定不能一刀切。我这里给一套经过实践验证的初始建议参数名主库写多从库读多说明initialSize510启动时预创建连接数从库可以多配minIdle510最小空闲连接防止高峰期创建连接延迟maxActive2030最大活跃连接数从库并发查询多时调大maxWait6000060000获取连接最大等待时间过长容易掩盖问题timeBetweenEvictionRunsMillis6000060000连接检测周期minEvictableIdleTimeMillis300000300000最小可回收空闲时间testWhileIdletruetrue空闲时测试连接是否有效testOnBorrowfalsefalse借出时不额外测试提高性能removeAbandonedtruetrue开启连接泄漏检测removeAbandonedTimeout180180超过180秒未归还视为泄漏需要提醒的是removeAbandoned只适合用来排查问题不适合长期开启。因为它可能中断正常的长事务导致业务异常。我一般会先在测试环境开启定位到泄漏代码后再在生产环境关闭。7. 多数据源动态切换的常见问题与排查技巧7.1 切换不生效SQL始终走主库这个问题概率最高主要原因有下面几种数据源key不对路由不到目标数据源兜底到默认主库。AOP切面没生效注解没被扫描或内部调用绕过了代理。切面顺序晚于事务切面连接已绑定。Primary没有加在动态数据源上MyBatis-Plus直接注入单个数据源。自定义SqlSessionFactory时setDataSource传入了具体的DruidDataSource。排查顺序建议是先看Druid监控中SQL落在了哪个数据源再从大到小排查代码问题。如果Druid监控里两个数据源都有同样的SQL执行记录那就说明确实发生了切换只是部分调用没切过去这时候就检查AOP代理是不是覆盖了所有调用路径。7.2 多数据源下Spring事务不回滚Spring事务默认基于单个数据源连接。当你在一个Transactional方法里既调用了主库的Mapper又调用了从库的Mapper那么从库的写操作不会受主库事务控制。一旦从库写入失败主库事务提交了就会造成数据不一致。如果项目里存在这种跨库写操作建议先设计为两个独立的事务方法通过本地消息表或事件补偿来保证最终一致性。如果非要用一个事务就必须引入XA或Seata。我在实际项目中更倾向于用“主库数据操作 事务消息”的方式而不是强制两个库在同一事务里因为后者性能和可用性代价都太高。7.3 连接池报错GetConnectionTimeoutException场景某个数据源连接被借出后没有归还直到连接池的最大活跃数被耗尽。此时调用方获取连接超时。这个错误和动态切换的关联性极强。我遇到过的情况是在Service里手动调用了DataSourceContextHolder.setDataSource之后执行查询抛出了异常finally块里只做了clear()但没有真正关闭该查询的数据库连接。如果MyBatis-Plus的Mapper查询在抛出异常后没有走完资源释放逻辑正常来说MyBatis会关闭连接但特定情况下可能出现问题连接池就会越来越少。我的经验做法是动态切换的所有操作都尽量依赖Spring管理的事务或MyBatis的SqlSessionTemplate自动释放连接。如果你用了编程式切换一定再显式调用SqlSession.close()或者确保外层有事务模板帮我们管理连接。在Druid配置中开启removeAbandoned后如果日志里有abandoned connection字样就能精准定位到泄漏代码。7.4 不同数据源下MyBatis-Plus的自动填充失效MyBatis-Plus的自动填充MetaObjectHandler是在插入或更新时自动设置创建时间、更新时间等字段。如果你在多数据源切换场景里配置了多个SqlSessionFactory每个factory都会注册自己的MetaObjectHandler吗?默认情况下MetaObjectHandler是全局单例如果数据源A和数据源B的表结构相同但某些字段选填不同自动填充逻辑可能会根据默认数据源的表结构来填充。我遇到过一个场景主库表有is_deleted字段从库表没有结果从库插入时自动填充了is_deletedSQL执行报错无此字段。解决思路是不要在同一个应用里让两个数据源的表结构差异过大。如果确实有差异可以把从库表设计成包含主库的全部公共字段或者在从库方案中直接写原生SQL而非依赖MyBatis-Plus的自动填充。7.5 多数据源的Mapper扫描管理很多项目把所有Mapper放在同一个包下然后只配置一个MapperScan此时如果两个数据源的Mapper接口完全一样甚至方法签名一样就会注入混乱。我的规范做法是不同数据源对应不同的Mapper包。比如主库Mapper放在com.example.mapper.master从库Mapper放在com.example.mapper.slave。然后创建两个SqlSessionFactory各自扫描自己的包并把对应数据源注入进去。但是这种方案一旦动态切换数据源路由数据源那一套就不好用了因为两个SqlSessionFactory会把数据源绑定死。如果你的场景是“每个Mapper固定走某一个库”那就用多SqlSessionFactory如果你的场景是“同一个Mapper可以根据条件走不同库”那就用动态数据源。这两种模式不要混合着用否则排查难度会成倍增加。我最终选择的方案是动态路由数据源作为主方案Mapper不区分数据源包所有Mapper接口都在同一个包下由路由数据源根据当前线程key决定库。这样做的好处是代码简单切换跟Mapper无关坏处是如果你一个Mapper方法只能走某一种库强行切换时会报错。因此业务设计上必须清楚每个方法适合走哪些库避免乱切换。8. 生产环境下的多数据源最佳实践与个人体会8.1 一定要在架构文档里画清楚数据源矩阵当你决定使用多数据源方案以后第一件事不是写代码而是画一张数据源矩阵表。表格至少包含数据源名称、用途、连接URL主机、所属团队、允许的读写权限、允许访问的Mapper列表、对应事务边界。这样后期排查问题可以快速定位而不是对着代码翻半天。我这边的习惯是每次新建一个数据源先在矩阵表里加一行然后才去配置文件加代码。多人协作时矩阵表还能避免其他同事误用你的从库连接串。你可以用Confluence或者Wiki维护也可以直接用Markdown放在项目docs目录里。8.2 不要为了“灵活”过度设计动态切换动态切换多数据源看似很酷但它会给系统增加额外复杂度。你会发现代码审查时多了一个需要关注的维度测试时每个接口需要证明它正确选择了数据源上线后还需要监控数据源切换是否发生异常。如果你的需求仅仅是报表库只读完全不需要动态切换直接用多个SqlSessionFactory固定绑定Mapper包就够了。我在另一个项目里见过团队花了两周做了全自动数据源路由通过表名前缀自动路由到不同库结果上线后发现很多SQL被路由到了错误的库最后不得不花了三倍的时间去修复。所以能静态就不要动态能隔离就不要交叉。8.3 监控告警一定不能少多数据源最大的风险在于“不可见”。一个查询是走了主库还是从库对业务无感但一旦主库慢查询拖垮了写业务或者从库连接池爆掉影响立即放大。因此生产环境里每个数据源都要有独立的监控指标和告警规则。Druid的监控数据可以通过JMX导出配合PrometheusGrafana展示。告警建议关注三个指标活跃连接数超过maxActive的80%、获取连接等待时间超过maxWait、慢SQL数量突然增加。这三个指标基本能覆盖多数据源下最常见的故障场景。8.4 后续扩展方向读写分离与动态数据源组合如果你的系统不仅有多数据源还需要读写分离可以在动态路由数据源之上再叠加一层主库路由到master从库路由到一个“读负载均衡数据源”。这个读负载均衡数据源内部可以包含多个只读数据源通过轮询或随机方式选择其中一个。实现方法也不复杂自定义一个DataSourceGroup实现DataSource接口内部维护一组DruidDataSource在getConnection时轮询返回。然后将其作为动态路由数据源target中的一个目标。这样做的收益是从库扩缩容时业务代码完全无感只需运维调整配置。我目前实践最多的是“动态路由 一主多从”的模式。主库负责写从库负责读从库之间没有强一致性要求允许秒级延迟。如果从库挂了Druid的fail-fast机制可以自动剔除坏连接业务短暂感知缓慢但不会立即崩溃。8.5 关于Druid多数据源说法的一点个人建议网上的教程五花八门很多是复制粘贴连数据源key都写错了。我写这篇博客的目的是希望你把多数据源当成一个架构设计问题来看待而不是简单贴一段配置。先问清楚自己项目里到底有几个数据库分别承担什么职责切换粒度和触发方式是什么事务边界在哪里连接池参数分别如何管理监控怎么联调把这几个问题想清楚代码反而是最简单的一环。我在实际项目中试过无数种排列组合踩过连接池溢出的坑也被ThreadLocal串库折磨过最后沉淀下来的规律就是动态切换多数据源这件事难度不在于写代码而在于边界管理。只要你的边界定得足够清楚代码随便怎么写都不容易出错边界模糊再完美的框架也救不了你。这也是我给所有即将接手的多数据源改造项目最大的忠告。