ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot动态多数据源实战:不重启也能随时切换数据库

Spring Boot动态多数据源实战:不重启也能随时切换数据库 多数据源这个需求在Spring Boot项目里太常见了但我发现很多人对“动态连接”的理解还停留在“写死两个DataSource配置”的阶段。真正运行起来能随时新增一个数据库、不需要重启、业务代码一调用就自动切到新库的方案才是今天这篇要分享的核心内容。涉及的核心关键词就三个多数据源、Spring Boot、动态连接但背后牵扯出来的问题远比想象的多——连接池管理、事务绑定、Bean生命周期、路由策略每个环节都有坑。这篇内容主要面向正在做读写分离、多租户SaaS、或者数据中台这类需要动态接入新业务库的Java开发人员属于那种“自己摸索半天不如有人直接告诉你哪里会炸”的实战经验总结。1. 多数据源动态连接方案的场景与整体设计思路1.1 典型业务场景光一个“多”字背后诉求完全不同先说场景。很多人一听到多数据源第一反应就是读写分离——主库写、从库读配两个DataSource用一个AOP切一下注解就完事了。这种属于“静态多数据源”数据源个数是固定的配置在application.yml里写死启动时加载好运行期间不会变。但真正让人头疼的是另一种数据源在运行时才出现。举个例子我之前做过一个数据中台的项目核心功能是接入集团下面几十个业务系统的数据库做一个统一的查询入口。业务系统是陆续上线的今天接入A系统下个月可能又冒出两个新系统数据库类型还不一样MySQL、PostgreSQL、SQL Server混着来。如果每接入一个库就要改配置、重新打包、重启服务那运维同学怕是要提着刀来找我聊天。再比如多租户SaaS系统每个企业客户一个独立数据库注册新客户的时候后台需要动态创建一个数据库并让这个租户的所有请求自动路由到新库。这种场景下数据源的数量和连接信息是在运行期动态增加的没法靠静态配置解决。还有一类场景是自动化运维平台、报表系统、数据可视化大屏这类产品核心功能就是让用户自己填数据库地址、账号密码系统测试连通性后就直接拉数据出报表。这种就是“纯动态”数据源从哪来用户填了才知道。所以“动态连接”这个词核心诉求其实有三个第一运行期能新增数据源不用重启服务第二能随时切换当前线程使用哪个数据源第三新增的数据源要能被现有DAO层无缝使用也就是要透明地融入Spring管理的数据源体系。1.2 静态配置方案为什么搞不定动态场景先看看最常见静态方案长什么样才能在对比中理解为什么动态方案要复杂得多。spring: datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/primary username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver secondary: jdbc-url: jdbc:mysql://localhost:3306/secondary username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver配合一个基于Spring的AbstractRoutingDataSource实现加上一个用ThreadLocal保存当前数据源标识的ContextHolder再用AOP切DataSource注解就构成了大多数静态多数据源方案的标准架构。这个方案本身没什么大毛病但它天生有两个死角第一个死角数据源列表必须在启动时确定。因为AbstractRoutingDataSource.getConnection()是根据Key从targetDataSources这个Map里取DataSource的这个Map在初始化时被构建运行期如果往Map里塞新数据源需要同时确保后续的getConnection()能拿到这本身可以做到但Spring的Bean生命周期和连接池初始化时序会让整个过程变得敏感。第二个死角数据源的构建参数通常来自配置文件。动态场景下配置文件根本不存在数据库IP、账号密码是用户提交上来的或者从配置中心动态拉取的。这就意味着你必须把“构建DataSource”这个动作从启动阶段搬移到运行阶段而且还要处理测试连通性、连接池超时参数、异常回滚这些乱七八糟的细节。不是我非要否定静态方案静态方案在数据源数量稳定的小项目里依然好用简单直接但动态场景下它确实不够用。所以核心结论只有一个动态连接方案的本质是用一个可扩展的数据源注册中心替代固定的配置列表结合运行时路由让系统在不停机的情况下随时接纳新的数据源并对业务透明。2. 核心实现原理AbstractRoutingDataSource与ThreadLocal的搭配2.1 Spring提供的路由DataSource为什么是关键地基AbstractRoutingDataSource这个类是Spring JDBC模块里留给多数据源方案的最重要地基。它本身也是个DataSource但它的getConnection()不是直接返回某个固定连接的而是先走一个抽象方法determineCurrentLookupKey()拿到一个Key再从维护好的targetDataSources这个MapObject, DataSource里取出真正干活的DataSource最后返回那个DataSource的连接。这个设计完美对应了“路由”这个动作。你只需要做两件事第一维护好这个Map让每个Key对应一个真实的DataSource第二在determineCurrentLookupKey()里返回当前线程应该使用的Key。至于事务、连接池、MyBatis怎么和DataSource交互Spring全部帮你屏蔽了。我刚才说的“运行期往Map里塞新数据源”其实就是对targetDataSources做更新然后重新初始化。这里面有个细节AbstractRoutingDataSource内部有个resolvedDataSources缓存在afterPropertiesSet()或者第一次路由时会把targetDataSources的引用固化到resolvedDataSources里所以动态新增数据源后必须重新触发一次afterPropertiesSet()让路由缓存失效重建。这个坑非常隐蔽后面我会单独讲。2.2 ThreadLocal保存当前数据源标识为什么不用方法传参先写一个最基础的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(); } }可能有人会问为什么不用方法参数把数据源Key一路传下去非要搞一个ThreadLocal这里要理解一下MyBatis、JdbcTemplate这类框架到底怎么拿连接的。以MyBatis为例一个SqlSession里的Executor在执行JDBC操作时会通过Environment.getDataSource()获取DataSource再调用DataSourceUtils.getConnection(dataSource)来拿连接。整个调用链根本不会经过你的业务方法参数你不可能中途把DataSource塞给MyBatis。Spring的事务管理DataSourceTransactionManager.doBegin()里同样是通过TransactionSynchronizationManager.bindResource()把DataSource和Connection绑定在一起。所以想让框架层在运行时感知“当前该用哪个数据源”最自然的方案就是用一个上下文变量来做隐式传递。ThreadLocal完美匹配这个需求每个线程有自己独立的副本线程隔离不会串业务代码里设置一次整个调用链任意地方想获取都能拿到。等一次完整的业务操作结束在finally块里清理掉避免线程池复用导致数据源错乱这个清理动作千万不能省。2.3 DynamicDataSource完整实现继承抽象类并重写路由逻辑public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceKey(); } }代码就这么几行但我必须强调一个容易被忽略的点determineCurrentLookupKey()返回null的语义。如果返回nullAbstractRoutingDataSource会走determineTargetDataSource()里的默认逻辑也就是返回defaultTargetDataSource指定的默认数据源。这其实是一个很好的回退机制——业务代码里忘了设置Key至少还能正常走默认库不至于直接空指针。在设计上这个特性可以引申出另一个更灵活的用法Key不一定要在代码里写死可以动态生成。比如存一个用户ID然后路由到一个“按用户分库”的数据库集群。类似ShardingSphere的分片思路只不过我们是自己用最轻量的方式实现控制力更强也更便于排查问题。3. 动态数据源注册中心的搭建与运行期切换实践3.1 注册中心设计用一个Map搞定数据源的生命周期管理有了路由DataSource还缺一个“注册中心”。这个注册中心本质上就是一个线程安全的Map存放所有已经被创建出来的数据源实例同时提供新增、获取、移除的接口。我一般把注册中心和DynamicDataSource做在一个配置类里让DynamicDataSource持有注册中心的引用这样路由和注册就完全打通了。来看一段完整的实现代码Component public class DynamicDataSourceRegister { private final MapString, DataSource dataSourceMap new ConcurrentHashMap(); private DynamicDataSource dynamicDataSource; private final ApplicationContext applicationContext; public DynamicDataSourceRegister(ApplicationContext applicationContext) { this.applicationContext applicationContext; } PostConstruct public void init() { // 优先加载Spring容器中已有的静态数据源作为默认数据源 DynamicDataSource dataSource new DynamicDataSource(); MapString, DataSource beansOfType applicationContext.getBeansOfType(DataSource.class); if (!beansOfType.isEmpty()) { beansOfType.forEach((beanName, ds) - { if (!(ds instanceof DynamicDataSource)) { dataSourceMap.put(beanName, ds); } }); } dataSource.setTargetDataSources(dataSourceMap); dataSource.setDefaultTargetDataSource(dataSourceMap.values().iterator().next()); this.dynamicDataSource dataSource; } public void addDataSource(String key, DataSource dataSource) { dataSourceMap.put(key, dataSource); // 关键一步必须重新设置并初始化路由缓存才会生效 dynamicDataSource.setTargetDataSources(dataSourceMap); dynamicDataSource.afterPropertiesSet(); } public DataSource getDataSource(String key) { return dataSourceMap.get(key); } public DynamicDataSource getDynamicDataSource() { return dynamicDataSource; } }我把这个类单独拿出来而不是直接在DynamicDataSource里弄一个静态Map是为了更清晰地划分职责。DynamicDataSource只负责“按Key路由”DynamicDataSourceRegister负责“管理Key到DataSource的映射关系”。后续如果要做数据源移除、健康检查、连接池预热都在注册中心里扩展路由类不用跟着改职责边界很清楚。3.2 动态构建DataSource解析用户填写的连接参数并完成注册注册中心有了接下来就是动态构建DataSource本身。这部分有个核心问题连接信息是运行期才有的你没法写死在配置里。常见做法是接收一个放连接参数的Map然后通过DataSourceBuilder来构建。public void registerDataSourceFromParams(String key, String url, String username, String password, String driverClassName) { MapString, String paramMap new HashMap(); paramMap.put(url, url); paramMap.put(username, username); paramMap.put(password, password); paramMap.put(driver-class-name, driverClassName); DataSource dataSource DataSourceBuilder.create() .type(HikariDataSource.class) .url(url) .username(username) .password(password) .driverClassName(driverClassName) .build(); DataSourceProperties properties new DataSourceProperties(); properties.setUrl(url); properties.setUsername(username); properties.setPassword(password); properties.setDriverClassName(driverClassName); // 构建Hikari连接池并设置关键参数 HikariDataSource hikariDataSource (HikariDataSource) dataSource; hikariDataSource.setMaximumPoolSize(10); hikariDataSource.setMinimumIdle(1); hikariDataSource.setConnectionTimeout(3000); hikariDataSource.setIdleTimeout(60000); hikariDataSource.setMaxLifetime(1800000); // 测试连通性失败则直接抛出异常防止脏数据源进入注册中心 try (Connection connection hikariDataSource.getConnection()) { // 能拿到连接说明参数OK } catch (SQLException e) { hikariDataSource.close(); throw new RuntimeException(数据源连接失败 e.getMessage(), e); } addDataSource(key, hikariDataSource); }这里有几个参数我必须单独拎出来说。connectionTimeout设成3000毫秒是为了让用户填错连接参数时能快速失败不用傻等默认的30秒超时。setMaximumPoolSize(10)、setMinimumIdle(1)这种值也不是拍脑袋定的数据中台里不同业务库的并发量差异很大如果都用默认最大10个连接几十个库就是几百个连接连接池本身的内存和数据库端资源都扛不住。所以我一般会在注册接口里让调用方顺便传入一个maxPoolSize参数而不是统一写死。另外还要注意一个细节DataSourceBuilder.create().build()返回的DataSource类型和你指定的.type(HikariDataSource.class)必须一致。如果项目里pom没有引入HikariCP这里会直接报类找不到。Spring Boot 2.x以后默认连接池就是HikariCP问题不大但如果是Spring Boot 1.x换了其他连接池这块就要自己跟着换。3.3 业务侧无缝切换手动和AOP两种姿势数据源注册完成后业务侧怎么用最直接的方式是手动调用DynamicDataSourceContextHolder.setDataSourceKey(order_db); try { // 执行查询或更新 orderMapper.selectById(1L); } finally { DynamicDataSourceContextHolder.clear(); }手动方式的优点是灵活适合“同一段逻辑里中途切换”的场景。但它有个问题如果每个业务方法都要手写try-finally代码就变得又丑又容易忘。所以更优雅的做法是结合AOP做一个注解和MyBatis-Plus生态里常见的DS注解思路差不多但我们可以完全自己控制。Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DataSourceSwitch { String value() default ; }Aspect Component public class DataSourceAspect { Around(annotation(dataSourceSwitch)) public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSourceSwitch dataSourceSwitch) throws Throwable { String dataSourceKey dataSourceSwitch.value(); DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey); try { return joinPoint.proceed(); } finally { DynamicDataSourceContextHolder.clear(); } } }AOP方式的优势不只是省掉模板代码更重要的是让“数据源路由”这个横切关注点和业务逻辑解耦。哪个方法走哪个库看一眼注解就一目了然新人接手代码也不容易搞错。不过使用AOP需要注意切入点的粒度。我见过有人把DataSourceSwitch打在Mapper接口上结果发现有时候生效有时候不生效排查半天。原因是Spring的AOP默认只对Spring管理的Bean生效而且Mapper接口的代理生成机制和普通Service还不完全一样。稳妥的做法是打在Service实现类的public方法上粒度精确到“以方法为单位的数据源选择”这也是业内最主流的实践姿势。3.4 配置中心联动让数据源配置彻底脱离本地文件动态注册还有一种进阶玩法和配置中心比如Nacos、Apollo联动。系统启动的时候从配置中心拉取一份JSON里面包含了所有已授权业务库的连接信息注册中心一次性把初始数据源全部加载。后续有新的业务库要接入只需要在配置中心新增一条配置通过监听机制触发registerDataSourceFromParams()连接就自动建好了连界面都不用动。这个方案的吸引力在于它把数据源管理和配置中心统一起来配置中心是“唯一事实来源”运行中的Spring Boot服务只是配置的执行者。而且配置中心本身就带版本管理和变更历史谁在什么时候改了连接信息都有迹可循避免了直接在服务器上改配置这种高危操作。我之前做数据中台的时候就是这样把Nacos和数据源注册中心串起来的。监听配置变更的伪代码大致是Component public class NacosDataSourceListener { private final DynamicDataSourceRegister register; public NacosDataSourceListener(DynamicDataSourceRegister register) { this.register register; } NacosConfigListener(dataId datasource-list.json, timeout 5000) public void onConfigChange(String configContent) { ListDataSourceConfigItem items JSON.parseArray(configContent, DataSourceConfigItem.class); for (DataSourceConfigItem item : items) { register.registerDataSourceFromParams( item.getKey(), item.getUrl(), item.getUsername(), item.getPassword(), item.getDriverClassName() ); } } }这里需要重点提的是增量还是全量更新的问题。如果监听方法每次都是全量注册那么老的连接池不会被关闭连接数就会像滚雪球一样越滚越多。所以实际项目里我会在注册方法里加一个“如果key已存在且连接参数没变化就跳过如果参数有变化先关闭旧连接池再创建新连接池”的逻辑这个细节直接关系到线上稳定性。4. 事务、连接池与MyBatis集成这些坑必须提前堵上4.1 事务绑定导致动态切换失效最常见也最容易翻车动态数据源方案里排第一位的坑就是事务。很多人会碰到这种情况Service方法上加了Transactional注解里面通过DynamicDataSourceContextHolder.setDataSourceKey()切换库结果发现切换完全无效查询还是走的老库。这背后的原理一定要搞明白。Spring的事务管理默认是在事务开启的那一刻就通过DataSourceTransactionManager从DataSource里拿了一个Connection然后用TransactionSynchronizationManager.bindResource()把连接绑定到当前线程。后续整个事务期间所有数据库操作都复用这同一个连接压根不会再去看路由规则。你中途切换了Key但连接已经绑定了等于路由规则换了、执行者没换自然就失效了。解决方案有这么几个思路。第一事务边界尽量缩到最小把Transactional只加在真正需要原子性的方法上数据源切换放在开启事务之前。第二如果必须在一个事务里操作多个数据源那就要考虑分布式事务这个复杂度就上来了通常不建议轻易引入。第三对于“切换数据源后执行一次查询就结束”的场景可以不用事务直接让Mapper去拿新连接。我自己的习惯是让动态数据源方案和声明式事务保持隔离。动态切换克制一点事务注解谨慎一点。大多数动态数据源的使用场景都是查询性质的本来也不强依赖事务把Transactional去掉之后问题自然消失。4.2 连接池泄漏动态注册容易关闭却常常被遗忘动态数据源方案另一个隐蔽问题是连接池泄漏。你通过注册中心往Map里塞了几十个HikariDataSource每个连接池默认维护着若干活跃连接。业务跑一阵子之后数据库端会发现来自你这个应用的连接数高得离谱甚至触达数据库的max_connections上限。连接数膨胀的原因通常有两个一是最小空闲连接设得太大比如每个数据源minimumIdle默认都是10注册5个库就是50个常驻连接二是同一个数据源被反复注册每次都是新的连接池实例旧的又没有close。针对这个问题我从三个维度做了控制。第一注册前先判断key是否已存在如果存在且连接参数没变化直接忽略本次注册。第二如果是参数变更需要重建连接池先把旧连接池close掉再插入新的。第三给动态数据源加一个HealthCheck机制定期检测连接是否可用长时间不用的数据源比如超过24小时没人查过一次自动从Map里移除并关闭连接池。这里贴一段“先关旧再上新”的代码供直接参考public DataSource addDataSource(String key, DataSource newDataSource) { DataSource oldDataSource dataSourceMap.put(key, newDataSource); if (oldDataSource instanceof HikariDataSource) { try { ((HikariDataSource) oldDataSource).close(); } catch (Exception e) { log.warn(关闭旧数据源失败key{}, key, e); } } dynamicDataSource.setTargetDataSources(dataSourceMap); dynamicDataSource.afterPropertiesSet(); return newDataSource; }注意这个put是先返回旧值再放新值所以必须先在Map里完成替换拿到旧引用后去close。顺序一定不能反过来否则中间有短暂的空窗期路由到不存在的DataSource会直接炸掉。4.3 MyBatis启动时自动扫描哪些Mapper不会因为新数据源受影响MyBatis集成这块很多人会有一个误区觉得动态新增数据源之后Mapper可能找不到对应的SqlSessionFactory或者Mapper扫描会出问题。其实完全不用担心MyBatis的核心模型是Mapper → SqlSessionFactory → DataSource这样的组合关系。在Spring Boot MyBatis的常规配置下SqlSessionFactory是启动时就构建好的它持有的是DynamicDataSource这个代理对象而不是某个具体数据库的连接。运行期注册中心更新的是DynamicDataSource内部的路由表SqlSessionFactory持有的引用始终是同一个DynamicDataSource。所以只要DynamicDataSource内部的路由数据保持正确所有Mapper自动就能感知到新数据源不需要任何额外的MyBatis配置。唯一要留意的场景是MyBatis-Plus这类增强框架里的分页插件、多租户插件。它们内部有可能缓存一些和DataSource相关的元数据比如方言类型。如果你的多个数据源之间数据库类型不一样MySQL和PostgreSQL混用分页插件解析方言时可能会拿默认配置去处理导致SQL语法不兼容。这种情况建议给每个数据源单独配置SqlSessionFactory或者使用支持动态方言的插件版本。4.4 Spring Boot版本和动态注册的兼容性Spring Boot 2.x和3.x在DataSource自动配置上有一些差异最典型的是DataSourceAutoConfiguration里的DataSourceProperties绑定方式变化以及Spring Boot 3.x里Jakarta命名空间的迁移。如果你的项目是Spring Boot 2.4以前的版本AbstractRoutingDataSource还支持通过targetDataSources直接装配到了高版本自定义实现的数据源路由类写法基本一致但要注意DataSourceBuilder构建时默认使用的连接池类型可能随版本变化。另外还有一个很现实的问题Spring Boot版本太高比如用了3.3、3.4这类新版本如果你手撸的动态数据源代码里用了javax.sql.DataSource这类老包编译会直接报错。Spring Boot 3.x全面切换到Jakarta命名空间是众所周知的但很多人不知道的是javax.sql.DataSource是Java EE标准包不是Spring Boot自己定的所以动态路由方案里你写的DataSource实现类只要基于JDK标准接口理论上是兼容的。真正容易踩坑的是那些直接依赖Spring内部工具类比如SpringUtils、DataBinder的写法这类代码在版本升级时最容易出现API不兼容。我个人的建议是不要盲目追逐最新版Spring Boot。多数据源动态注册这种底层方案稳定压倒一切选一个自己熟悉的、社区生态成熟的版本然后把核心逻辑封装成独立模块减少和Spring版本之间的耦合这样后续升级也好控制影响范围。5. 典型问题排查实录与性能优化要点5.1 切换不生效优先检查这六个位置动态数据源切换不生效线上表现就是“该走A库的结果走了B库”或者“A库报了表不存在的错误”。我一般按照下面这个顺序排查效率比较高ThreadLocal的值有没有被提前clear看看是不是有拦截器或AOP切面在方法执行前就调用了clear()把Key给清掉了。有没有开启事务如果Transactional已经开启连接在事务开始时绑定了切换自然不会生效。这个我一开头就强调过排第一位的嫌疑犯。数据源Key是否正确注册注册中心Map里有没有这个Key打印一下dataSourceMap.keySet()最直接。增删数据源后有没有重新afterPropertiesSet()只在Map里put了数据源但没触发路由缓存重建新数据源不会生效。AOP切面是否生效注解所在方法是不是被Spring代理如果是this.xxx()内部调用注解不会走代理逻辑。多线程环境下ThreadLocal隔离ThreadLocal不是全剧共享的如果切换Key之后Async异步线程或者MQ消费者线程里继续执行SQL它们拿不到主线程的Key这是设计问题需要显式传递。第6点尤其容易忽略。比如用了Async开启异步异步线程里的查询用的是默认数据源而不是你设好的那个Key。解决思路有两种一种是把Key作为参数传给异步方法在异步方法入口再设置ThreadLocal另一种是直接用TransmittableThreadLocal这类支持线程间值传递的方案不过会增加包依赖能不用尽量不用。5.2 数据源连接池启动失败导致服务不可用动态注册的数据源参数千奇百怪不可能保证每次都是正确的。用户填的IP错了、端口不通、密码过期、数据库本身拒绝连接这些情况都会导致DataSourceBuilder.build()构建出来的连接池在首次getConnection()时抛异常。如果注册是在启动阶段执行的一个坏数据源就可能让整个Spring Boot应用启动失败这是不可接受的。我的处理办法是注册动作绝不让它影响主流程。启动时单独包一个try-catch捕获异常后只打印警告日志把这个数据源标记为“异常状态”然后跳过其他正常数据源照常注册加载。等用户后续修改连接参数再触发一次注册流程把这个数据源从异常状态恢复过来。同时把异常明细记录下来方便在管理界面上直接展示给用户。5.3 连接池参数如何配置才能兼顾性能与资源占用动态数据源场景下连接池参数不能图省事全部用默认值。我整理了一套自己常用的配置模板参数项参考值配置理由maximumPoolSize5~20按业务并发量预估宁可小一点也别让几十个库抢占数据库端连接数minimumIdle1~2数据源可能很久不用常驻空闲连接浪费资源connectionTimeout3000ms快速失败避免用户填错参数后长时间卡住idleTimeout60000ms空闲连接尽快释放配合minimumIdle1maxLifetime1800000ms低于数据库wait_timeout防止连接被极端机制回收validationTimeout1000ms配合Hikari的testOnBorrow检测连接有效性这套参数的核心思路是“宁缺毋滥”。因为动态数据源具有不确定性你不知道这个库后面会被多少人用、多久用一次保守一点连接池配置避免把数据库连接数打满比极限性能更重要。等某个数据源的使用量上去了再单独调整它的连接池参数也不迟。5.4 数据源下线与动态移除动态注册是正向操作很多人想不起来做反向操作——数据源下线。如果某个业务系统被下线了、数据库要迁移到新的集群老的连接信息还残留在注册中心里连接池会一直建立连接失败重试日志刷屏不说还白白占着线程。我建议注册中心提供三个基础方法注册、更新、移除。移除的时候同样要注意先关闭连接池再删除Map条目顺序不要反。另外移除操作之后也要调用dynamicDataSource.afterPropertiesSet()刷新路由缓存否则Mapper还是能通过路由表老Key拿到已经关闭的DataSource报出Connection is not available之类异常。5.5 性能优化减少路由查找开销与数据源预热最后说性能。动态路由相比直接注入单数据源确实多了一层Map查找和getConnection()的分发但这个开销小到可以忽略不计。真正需要优化的是两个地方第一避免在循环内频繁切换数据源。如果你要循环处理100条记录每条记录属于不同的库千万不要每条记录都setDataSourceKey查询。正确做法是先把记录按数据源分组切到一个库批量查完再切到下一个库。这样既高效又避免连接池频繁创建新连接。第二提前预热关键数据源。连接池的首次getConnection()会触发连接创建虽然Hikari很快但在高并发入口处还是会产生一个毛刺。可以在动态注册完成后主动执行一次hikariDataSource.getConnection().close()把连接池预创建出来。实际压测下来这个预热操作能把首次查询的RT从几十毫秒降到几毫秒收益还是挺明显的。我曾经用这个方法优化过一个报表系统的数据库接入功能几十个数据源轮询查询的场景下预热之后整体查询耗时下降了接近35%没有改一行SQL单纯靠连接池预热性价比极高。我的几点实操心得与提醒多数据源动态连接方案做到现在我自己最大的感受是这个方案本身不复杂难的是驾驭它周围的“环境变量”。事务、连接池生命周期、Spring Bean作用范围、异步线程传值每一个单独拎出来都是经典问题组合在一起就变成了深水区。没有哪个框架能替你解决所有问题最终还是要对原理有足够清楚的认识。最后再分享一个小技巧也是我后来形成的习惯所有对注册中心的操作不管成功还是失败一律按key打业务日志记录变更前后的连接URL、操作类型、耗时。多数据源本身就是一个多条线索交织的系统等线上出问题的时候这些日志就是你最快的定位抓手。没有日志全靠猜那才是真的麻烦。
返回列表