☰
SpringBoot动态数据源切换与Nacos配置中心实战指南
2026/10/24 7:12:57 网站建设 项目流程

“SpringBoot 动态数据源切换(Nacos做配置中心)”,这个标题搁在一年前我可能还会觉得是个值得写篇博客的“高级玩法”,但现在它基本属于微服务架构下的必备技能了。尤其是当业务量上来之后,“一个应用只连一个库”这种老黄历配置已经撑不住场景了——读写分离要切换、多租户要切换、分库分表要切换、连测试环境联调都要切换。光“能连多个库”远远不够,关键是要“动态切换”,而且是改完配置不用重启、不丢连接、不出脏数据那种。

我最早接触这个需求,是因为一套运营后台要支持多个业务线的数据源动态挂载。当时的痛点是:新增一条业务线,就要改配置文件、重启应用、重新发布——运维同学直接跟我翻脸。后来决定把数据源配置交到Nacos上,让配置中心和动态切换一起搞定。这篇文章我就把这套完整方案从头到尾拆一遍,核心踩过的坑也会一并写出来。

1. 项目背景与整体方案设计

1.1 什么场景下真的需要动态数据源切换

先别急着写代码,先搞清楚你到底是不是真的需要“动态”切换。如果只是静态多数据源,两个库固定连好、按业务路由就完事,根本不需要搞一套动态方案。真正逼着你上“动态”的,通常是这三类场景:

  • 多租户平台:每个客户一个独立库,租户数量不固定,新建租户时不能让应用重启,必须运行中自动挂载新数据源。
  • 热点库容灾切换:某个核心库压力过高或运维要迁移库,需要通过配置中心下发切换指令,把读写流量从旧库瞬时切到新库。
  • 测试/联调环境灵活切换:联调阶段需要临时把某个下游库切到测试环境地址,之后又切回生产库,全链路由一个配置开关控制。

我遇到的主要是第一种。运营后台要给新客户开库,数据库建好后配置一改,数据源自动挂载,全程不用动代码、不用重启、不用发版。这就涉及两个核心能力:一是“多数据源动态路由”,二是“通过Nacos感知配置变化并实时重建数据源”。

1.2 为什么选Nacos而不是其他配置中心

市面上能做配置中心的不止Nacos一个,Spring Cloud Config、Apollo也都是成熟方案。我这次选Nacos的原因很朴素:

  • 部署轻量:单机模式一个jar包直接启动,不需要额外依赖数据库(暂存模式),小团队运维成本低。
  • 配置变更推送及时:Nacos采用服务端主动推送加客户端长轮询结合的机制,配置发布后秒级生效,满足数据源热切换的时效要求。
  • 和Spring生态集成顺滑:Spring Cloud Alibaba对Nacos支持完善,接入成本低。
  • 自带控制台:配置变更、历史版本、回滚操作都能在Web界面完成,非技术同事也能操作。

当然Apollo在配置管理和权限控制上更重更完善,更适合大型组织。但就“数据源动态切换”这个需求本身来说,Nacos的轻量已经足够。而且我不建议把整套业务都跟某个配置中心深度绑定,自己实现一层“配置监听 + 数据源重建”的逻辑,反而能把耦合控制在最小范围。

1.3 整体方案架构

整套方案的运行链路可以概括为:

  • Nacos上维护一份数据源配置文件(JSON格式),核心内容包含多个数据源节点,每个节点有唯一标识key、数据库类型、连接地址、账号、密码、连接池参数。
  • 应用启动时,从Nacos拉取这份配置,解析后逐个创建对应的DataSource,注册进一个“动态数据源路由器”。
  • 业务代码通过注解或手动指定数据源key,在方法调用时动态路由到目标库里执行SQL。
  • Nacos配置发生变更时,监听器收到事件,重新解析配置,对比新旧数据源列表,新增的挂载进去、删除的移除并关闭、变更的连接池参数则重连更新。

这里面最核心的两个技术点是:如何用路由机制实现在同一个事务链路里自由切换数据源,以及如何安全地重建和销毁连接池。下面逐个拆。

提示:整个方案的复杂度不在“能连多个库”,而在于“切换的时候不出事”。连接不释放、旧库连接泄漏、事务边界错乱、配置变更时并发访问中断,这些才是实战中最容易翻车的地方。

2. 动态数据源切换的核心实现原理

2.1 路由入口:AbstractRoutingDataSource的工作原理

Spring框架本身就给了一个轮子——AbstractRoutingDataSource。它本质上是一个转发器:本身并不是真正的数据源,但它维护着一个目标数据源Map,每次获取连接时根据路由key找到对应的真实数据源,再从真实数据源里拿连接。

这里的关键方法是determineCurrentLookupKey(),返回的是当前线程应该使用哪个数据源的标识。这个标识怎么和当前业务线程绑定?通常用ThreadLocal:

public class DynamicDataSourceContextHolder { 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(); } }

有了上下文持有器,再实现路由数据源:

public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>(); @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceKey(); } @Override public Connection getConnection() throws SQLException { // 如果未指定数据源key,走默认数据源 String key = determineCurrentLookupKey(); if (key == null || !dataSourceMap.containsKey(key)) { return determinePrimaryDataSource().getConnection(); } return dataSourceMap.get(key).getConnection(); } public void addDataSource(String key, DataSource dataSource) { dataSourceMap.put(key, dataSource); } public void removeDataSource(String key) { dataSourceMap.remove(key); } }

这样设计之后,业务里切换数据源的姿势就很简单了:方法入口setKey,方法执行完后clear。

但这里有个非常隐蔽的坑:AbstractRoutingDataSource在初始化时会把目标数据源建好,如果你在运行期动态往Map里塞新的DataSource,路由能生效,但连接获取不会走你新加的那个——因为AbstractRoutingDataSource底层会把目标数据源包装成ConnectionHolder提前缓存。所以实际项目中我不建议直接继承它做动态增删数据源,而是自己维护一个“注册中心Map”,在getConnection里手动路由。上面的代码就是规避这个问题的写法。

2.2 原理解读:为什么用ThreadLocal而不是参数传递

有些读者可能会问:为什么不用方法参数把数据源key传下去,非要搞个ThreadLocal?答案是:业务链路里并不是每一层都能感知到“要切换到哪个库”。比如一个Service调用DAO,DAO里的Repository基类在获取连接时必须要知道当前请求属于哪个租户,但你不能在每个Repository方法里都加一个数据源参数,否则接口签名全部污染。

ThreadLocal的本质是线程级上下文,只要在请求入口处set一次,后续所有调用栈中的代码都能读取到同一个值。它解决的是“跨方法隐式传递状态”的问题,代价则是“显式清理”的负担。忘记clear会导致线程池复用线程时把数据源key串到别的请求上,这是动态数据源切换最常见的生产事故。

所以我的建议是:永远使用AOP或过滤器统一管理set和clear,绝不在业务代码里散落手动set。包里切面做掉,避免人为遗漏。

@Aspect @Component public class DynamicDataSourceAspect { @Around("@annotation(dataSource)") public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSourceSelector dataSource) throws Throwable { String key = dataSource.value(); DynamicDataSourceContextHolder.setDataSourceKey(key); try { return joinPoint.proceed(); } finally { DynamicDataSourceContextHolder.clear(); } } }

2.3 动态挂载数据源:连接池的创建与销毁

数据源本身不是new一个对象就完事的,背后是连接池。以HikariCP为例,创建一个连接池涉及初始连接创建、空闲连接预热、连接参数验证等动作。直接new一个HikariDataSource出来,虽然也能用,但生产环境我强烈建议把连接池的初始化参数也放在Nacos配置里下发,这样才能应对“需要动态调整最大连接数”这类场景。

核心实现逻辑:

public DataSource buildDataSource(DataSourceConfig config) { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(config.getJdbcUrl()); hikariConfig.setUsername(config.getUsername()); hikariConfig.setPassword(config.getPassword()); hikariConfig.setDriverClassName(config.getDriverClassName()); hikariConfig.setMaximumPoolSize(config.getMaxPoolSize()); hikariConfig.setMinimumIdle(config.getMinIdle()); hikariConfig.setConnectionTimeout(config.getConnectionTimeout()); hikariConfig.setIdleTimeout(config.getIdleTimeout()); return new HikariDataSource(hikariConfig); }

销毁则必须找到旧连接池并显式close:

public void destroyDataSource(String key) { DataSource oldDataSource = dataSourceMap.remove(key); if (oldDataSource instanceof HikariDataSource) { ((HikariDataSource) oldDataSource).close(); } else if (oldDataSource instanceof DruidDataSource) { ((DruidDataSource) oldDataSource).close(); } }

注意:直接替换Map里key对应的DataSource,会导致旧连接池无法被垃圾回收,连接池内部的数据库连接会一直占着数据库连接数上限,最终把数据库连接池打爆。生产上我见过不止一次这种“只更新不销毁”导致的连接泄漏事故。

3. Nacos配置中心设计与配置管理

3.1 Data ID与配置格式规划

配置中心的设计决定了后期运维是否顺手。我的建议是:Data ID按照“应用名-环境-用途”的规则命名,Group按业务线划分,Namespace按环境隔离。

以一个实际项目为例:

  • Namespace:prod、test、dev
  • Group:DEFAULT_GROUP
  • Data ID:dynamic-datasource.json

配置文件内容用JSON格式,因为JSON天然能表达“一组数据源”的结构,后续做diff对比也方便:

{ "defaultKey": "order-primary", "datasources": [ { "key": "order-primary", "driverClassName": "com.mysql.cj.jdbc.Driver", "jdbcUrl": "jdbc:mysql://10.0.1.10:3306/order?useUnicode=true&characterEncoding=utf8", "username": "root", "password": "encrypted_password", "maxPoolSize": 20, "minIdle": 5 }, { "key": "order-replica", "driverClassName": "com.mysql.cj.jdbc.Driver", "jdbcUrl": "jdbc:mysql://10.0.1.11:3306/order?useUnicode=true&characterEncoding=utf8", "username": "root", "password": "encrypted_password", "maxPoolSize": 30, "minIdle": 5 } ] }

3.2 密码安全处理:别把明文密码放在配置中心

把明文数据库密码放在Nacos上,等于是把钥匙挂在门上。虽然Nacos控制台本身有鉴权,但运维后台往往不止一个人能看到配置,这种做法风险极高。

我的做法是:配置中心存放密文,应用启动时用对称密钥解密,密钥通过环境变量注入。这样就算Nacos配置被误读了,拿到的也是一串无法直接使用的密文。

@Component public class DecryptUtil { @Value("${datasource.secret.key}") private String secretKey; public String decrypt(String encryptedText) { // AES解密逻辑 return plainText; } }

3.3 Nacos监听实现:配置变更实时感知

Nacos客户端提供了ConfigService.addListener()方法,可以注册配置监听器。当配置在控制台上被修改发布后,客户端会收到变更通知。这个监听回调是在Nacos客户端线程里执行的,需要注意线程安全。

@Component public class NacosDataSourceConfigListener implements InitializingBean { @Autowired private ConfigService configService; @Autowired private DynamicDataSourceManager dataSourceManager; private static final String DATA_ID = "dynamic-datasource.json"; private static final String GROUP = "DEFAULT_GROUP"; @Override public void afterPropertiesSet() throws Exception { // 先拉取一次配置,完成初始化 String config = configService.getConfig(DATA_ID, GROUP, 5000); dataSourceManager.refreshDataSource(config); // 注册监听器,后续变更实时更新 configService.addListener(DATA_ID, GROUP, new Listener() { @Override public Executor getExecutor() { return Executors.newSingleThreadExecutor(); } @Override public void receiveConfigInfo(String configInfo) { dataSourceManager.refreshDataSource(configInfo); } }); } }

这里我特意用了newSingleThreadExecutor(),保证配置变更事件串行处理。如果并发处理配置变更,比如两次变更同时到达,“先删旧库再建新库”和“先建新库再删旧库”的执行顺序不同,可能导致连接断掉或数据源临时不可用。串行化最稳妥。

4. 核心代码实现与实操记录

4.1 动态数据源管理器:核心代码实现

数据源管理器的职责是:解析配置、对比变更、执行新增/更新/删除。这是整个方案的大脑,也是最容易出细节问题的地方。

@Component public class DynamicDataSourceManager { private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceManager.class); private final DynamicRoutingDataSource routingDataSource; private final Object lock = new Object(); private volatile Map<String, DataSourceConfig> currentConfigMap = new HashMap<>(); public DynamicDataSourceManager(DynamicRoutingDataSource routingDataSource) { this.routingDataSource = routingDataSource; } public void refreshDataSource(String configJson) { DataSourceConfigDTO dto = JSON.parseObject(configJson, DataSourceConfigDTO.class); Map<String, DataSourceConfig> newConfigMap = dto.getDatasources().stream() .collect(Collectors.toMap(DataSourceConfig::getKey, Function.identity())); synchronized (lock) { // 1. 需要新增的数据源 List<DataSourceConfig> addedConfigs = dto.getDatasources().stream() .filter(c -> !currentConfigMap.containsKey(c.getKey())) .collect(Collectors.toList()); // 2. 需要删除的数据源 List<String> removedKeys = currentConfigMap.keySet().stream() .filter(key -> !newConfigMap.containsKey(key)) .collect(Collectors.toList()); // 3. 需要更新的数据源(配置内容有变化) List<DataSourceConfig> updatedConfigs = dto.getDatasources().stream() .filter(c -> currentConfigMap.containsKey(c.getKey())) .filter(c -> !currentConfigMap.get(c.getKey()).equals(c)) .collect(Collectors.toList()); // 先处理新增 for (DataSourceConfig config : addedConfigs) { DataSource ds = buildDataSource(config); routingDataSource.addDataSource(config.getKey(), ds); log.info("新增数据源: {}", config.getKey()); } // 再处理更新(先建新连接池,再切换,再关旧连接池) for (DataSourceConfig config : updatedConfigs) { DataSource oldDs = routingDataSource.getDataSource(config.getKey()); DataSource newDs = buildDataSource(config); routingDataSource.addDataSource(config.getKey(), newDs); closeQuietly(oldDs); log.info("更新数据源: {}", config.getKey()); } // 最后处理删除 for (String key : removedKeys) { DataSource oldDs = routingDataSource.removeDataSource(key); closeQuietly(oldDs); log.info("删除数据源: {}", key); } currentConfigMap = newConfigMap; } } private void closeQuietly(DataSource dataSource) { try { if (dataSource instanceof HikariDataSource) { ((HikariDataSource) dataSource).close(); } else if (dataSource instanceof DruidDataSource) { ((DruidDataSource) dataSource).close(); } } catch (Exception e) { log.error("关闭旧数据源失败", e); } } }

更新数据源的顺序是个容易踩坑的细节。有人会为了省事直接关旧的再建新的,这个顺序在流量低谷期可能没事,但在高峰期会出现“旧库已关,新库还没建好”的空窗期。大量SQL执行失败、事务回滚,非常难看。正确顺序是先建新连接池,然后把路由切到新连接池,最后再关闭旧连接池,也就是上面代码里的“先建-后切-再关”。

4.2 业务注解与AOP切面:如何优雅地在业务代码里切换数据源

有了动态数据源管理器,再配合注解使用就方便了。自定义注解:

@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSourceSelector { String value() default ""; }

业务代码里使用:

@Service public class OrderService { @Autowired private OrderDao orderDao; @DataSourceSelector("order-primary") public void createOrder(OrderDO order) { orderDao.insert(order); } @DataSourceSelector("order-replica") public List<OrderDO> queryOrderList(String userId) { return orderDao.selectByUserId(userId); } }

AOP切面在方法执行前set数据源key,方法执行完毕后clear。不过要注意,如果AOP切面和@Transactional同时存在,切面必须保证数据源切换事务管理器可见。最佳实践是:@DataSourceSelector的切入顺序在@Transactional之前。

4.3 默认数据源与未命中路由的处理策略

数据源切换最常见的隐患是“指定的key不存在”。比如配置中心下发时漏掉了某个数据源,或者租户ID对应的数据源还没创建,这时候如果路由直接返回null,最终拿到连接时就会抛出空指针或“找不到数据源”的异常。

我推荐的处理策略是分级降级:

@Override public Connection getConnection() throws SQLException { String key = DynamicDataSourceContextHolder.getDataSourceKey(); if (key == null) { returndefaultDataSource.getConnection(); } DataSource ds = dataSourceMap.get(key); if (ds == null) { log.warn("数据源key[{}]不存在,降级到默认数据源", key); return defaultDataSource.getConnection(); } return ds.getConnection(); }

降级到默认数据源虽然可能拿到错误的库,但至少不会导致请求直接失败。如果对这个“错误路由”零容忍,那也可以选择直接抛异常,让上层感知到配置缺失。到底选哪种,我建议看业务属性:查询类操作可以降级,写操作必须严格校验,因为一旦把“A租户的数据写进B租户的库”,后果几乎无法挽回。

4.4 事务边界下的数据源切换:最容易被忽视的致命问题

在一个事务方法中,Connection一旦被创建,事务管理器就会把它绑定到当前线程。如果你在事务执行中途切换了数据源key,下一次获取连接时路由到了另一个数据源,但事务管理器以为还是同一个事务在跑,此时会出现两种情况:

  • 如果不同数据源之间没有分布式事务,第一个数据源上已经执行的SQL会被自动提交(或保持未提交状态),第二个数据源上的SQL会开启一个新事务,最后事务提交时只会提交第二个数据源上的事务,第一个数据源上的操作就悬空了。
  • 更隐蔽的是,同一个事务中拿到两个不同的Connection,事务管理器根据ThreadLocal里的连接映射,可能直接复用之前绑定的连接,或者因为“没找到绑定的连接”重新获取一个。无论哪种情况,都会产生“看似在一个事务里,实际拆成了两段”的脏数据问题。

规避方案有两条路:

方案一:切换数据源的操作放在事务方法外层

@DataSourceSelector("order-primary") public void createOrderWithTransaction(OrderDO order) { // 这里不能有事务,数据源切换后,再调用真正的事务方法 orderTxService.createOrderTx(order); }

方案二:基于编程式事务手动控制边界

public void createOrderManual(OrderDO order) { DynamicDataSourceContextHolder.setDataSourceKey("order-primary"); TransactionTemplate txTemplate = ...; txTemplate.executeWithoutResult(status -> { orderDao.insert(order); }); DynamicDataSourceContextHolder.clear(); }

如果非要在一个方法里同时切换数据源并保持事务,我的建议是彻底放弃声明式事务,改用编程式事务,自己手动控制每一个数据源上的事务提交和回滚。但这要非常小心地处理多个库之间的原子性问题,如果没有分布式事务组件兜底,就尽量别让单个业务方法跨多个数据源写数据。

5. 实际踩坑记录与问题排查

5.1 配置变更后连接池不释放

这个问题我在很多团队里都见过。把Nacos配置更新了,控制台日志也显示“数据源更新成功”,但数据库服务端的连接数一直涨,没有下降趋势。定位下来基本是同一个原因:更新数据源时只替换了Map里的引用,没有关闭旧连接池。

解决方案我已经在DynamicDataSourceManager里写出来了——更新和删除数据源时必须显式关掉旧DataSource。如果你用Druid,关闭是((DruidDataSource) oldDs).close();如果你用HikariCP,是((HikariDataSource) oldDs).close()。核心就一句:旧的连接池只要不调close,它内部的物理连接就不会归还给数据库,垃圾回收也收不回去。

5.2 ListenableFuture与线程池复用导致数据源串线

ThreadLocal的数据源key在异步线程里会失效。因为异步线程是从线程池取出来的,它根本没有经历我们AOP切面的set操作,所以ThreadLocal.get()拿到的是null,路由自动走了默认数据源。这在同步调用里发现不了,一上异步就出“查错库”的诡异问题。

我踩过一次:一个报表服务里用@Async调用一个查询方法,方法上标了@DataSourceSelector("report-ds"),但@Async会让方法在代理线程池里执行,AOP切面确实被触发了(注解还在方法上),但因为线程池复用的关系,前一个任务可能在finally块里把ThreadLocal给clear了,后一个任务进来时拿到的是上一任残留的key。

规避手段很简单:异步方法内部再次显式指定数据源,不要依赖外层切面。

@Async public void asyncExport(String tenantId) { DynamicDataSourceContextHolder.setDataSourceKey(tenantId); try { // 查询逻辑 } finally { DynamicDataSourceContextHolder.clear(); } }

5.3 连接池参数调优:为了切换而切换的后果

动态数据源切换最忌讳的是一股脑把每个库的连接池都配得很大。假设你配置了20个数据源,每个库maximumPoolSize=20,应用启动时虽然HikariCP是懒加载的,不会一下建400个连接,但一旦每个数据源都被访问过一次,数据库那边就要扛住400个连接。如果数据库部署在同一台机器上,连接数直接吓死人。

实际调参经验:

  • 核心库的连接池配置大一些,maximumPoolSize=20~30即可,不要盲目往50、100去调,HikariCP官方就叫“Fast, simple, reliable”,连接池太大反而增加获取连接的排队时间。
  • 边缘业务的数据源连接池尽量小,maximumPoolSize=5~10就够。
  • minIdle别设太高,尤其数据源总数量多的场景,Idle连接太多纯属浪费数据库资源。
  • 配置变更时如果只是改连接池参数,不建议销毁重建整个数据源。HikariCP提供了运行时动态修改部分参数的方法,比如setMaximumPoolSize(),能避免重建连接池带来的瞬时断连损耗。

5.4 配置中心宕机或网络隔离时怎么办

当应用和Nacos之间的网络断了,配置监听的实时性就失效了。如果此时正好要切换数据源,应用用的还是内存里的旧配置。大部分场景下这不是问题,但有一种情况非常麻烦:数据库密码轮换,配置中心已经更新了,但应用连接池里的还是旧密码,长期不重连就永远用旧密码。

Nacos客户端对此有兜底机制:配置变更时服务端会推送,客户端本地有缓存快照。就算服务端暂时连不上,客户端也会用本地缓存。但“缓存”的是配置内容,不是连接池状态。所以我的建议是:数据库密码的变更尽量配合应用发布或手动触发一次应用内刷新,不要完全押注在配置中心的实时性上。

5.5 常见问题速查表

问题现象可能原因排查方法解决方案
切换数据源后SQL执行报“No database selected”数据源key对应配置的jdbcUrl缺少库名检查Nacos配置中的jdbcUrl在jdbcUrl中写明scheme,如jdbc:mysql://host:3306/dbname
配置更新后部分连接仍然指向旧库连接池中存在未被回收的旧连接数据库端查show processlist看连接来源更新数据源时先建新连接池切流量再关旧连接池
偶发出现“Connection is not available”等待超时某个库的连接池被打满查HikariCP连接池状态指标,看活跃连接数适当调大maximumPoolSize或排查慢SQL
方法A设置了数据源,但方法B查询数据不对方法A抛异常后没有执行clear检查AOP切面是否用finally清理AOP的清理逻辑必须放在finally块里
多线程并发下数据源key互相串ThreadLocal变量被污染检查是否有阻塞操作跨线程传递key使用TransmittableThreadLocal或确保每个线程都显式set
异步线程内查的数据是默认库的@Async线程拿不到父线程的ThreadLocal打印日志观察异步线程里key是否为null异步方法内显式set指定数据源

5.6 一套用于快速验证的数据源切换日志规范

动态数据源切换出问题最难查的就是“当前请求到底用的是哪个库”。我强烈建议在数据源路由入口打一条带traceId的日志,这样排查问题时按traceId一搜,就能看到整个请求链路里每一次获取连接用的数据源key。

我实际项目里的日志格式大概是这样:

[DS-ROUTE] traceId=abc123, key=order-primary, db=10.0.1.10:3306/order, method=OrderService.queryOrderList

注意点:

  • key要记录业务代码指定的那个key,而不是解析后的地址,这样排查时能直接对应到配置中心里的某一项配置。
  • 数据源获取连接时记录连接池活跃数,方便判断是否接近连接池上限。
  • 切换前、切换后的数据源key都记录,能看到是否发生了“同请求内多次切换”的情况。

这套日志加上Nacos控制台的发布历史,一般线上问题半小时内都能定位。

最后聊两句个人体会

动态数据源切换这个东西,代码量不大,真正难的是想清楚各种边界情况。我前前后后改了三版,第一版只在Proxy数据源上做文章,结果事务边界一塌糊涂;第二版加上连接池销毁逻辑,又遇到配置更新后偶发连接断开的惊吓;第三版才把“先建新-再切换-后关旧”的顺序固化下来,把ThreadLocal清理全部收敛到AOP里,才算稳定。

如果你也是第一次接触这个方案,我建议先把最小闭环跑通:一个Nacos、一个动态数据源管理器、一个注解、一个默认库,先把切换跑顺了,再慢慢往上加复杂逻辑。

最后再分享一个小技巧:Nacos配置发布之后,如果担心线上出问题,最好先在控制台配置“灰度发布”,只对一台机器生效,观察3到5分钟没问题了再全量发布。数据源切换虽然底层是连接池重建,但任何一次切换都意味着存量连接要经历一次“青黄交接”,谨慎一点不会错的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询