手写一个Nacos配置中心:搞懂长轮询推送机制(附完整源码)

起因

前几天写了一篇文章,讲我们项目里遇到的一个配置刷新问题。简单说就是用了 @RefreshScope 之后,简单的字符串、数字能自动刷新,但复杂的嵌套 Map 就不行了。后来我们自己监听了 EnvironmentChangeEvent,手动用 ResolvableType 绑定才搞定。

写完那篇文章之后,突然就很好奇:

  • Nacos 配置中心到底是怎么做到的? 我在 Nacos 控制台改个配置点发布,几秒钟后我的应用就自动生效了,这中间发生了什么?
  • 是服务端推送还是客户端拉取? 如果是推送,服务端怎么知道哪些客户端需要通知?如果是拉取,客户端怎么知道配置变了?
  • 为什么有时候感觉立刻就生效,有时候要等几秒? 这个延迟是怎么回事?

带着这些疑问,我决定自己动手写一个简化版的 Nacos 配置中心,看看能不能把这些机制都搞明白。

写之前先猜猜

我一开始以为 Nacos 用的是 WebSocket 或者 Server-Sent Events (SSE) 这种长连接方案。毕竟配置一变就要通知客户端,感觉上应该是服务端主动推送吧?

后来翻了翻资料才发现,人家用的是 HTTP 长轮询

什么是长轮询?简单说就是:

  • 客户端发一个 HTTP 请求给服务端
  • 服务端不立刻返回,而是把这个请求 hold 住
  • 如果配置变了,立刻返回变更信号
  • 如果一直没变,就等到超时(比如 30 秒)再返回空结果
  • 客户端收到响应后,等个几秒,再发下一轮请求

这个机制挺巧妙的,我们后面详细说。

长轮询 vs 长连接 vs 短连接

这里先把几个容易混淆的概念理清楚。

graph TB subgraph 短连接 A1[客户端] -->|请求| B1[服务端] B1 -->|立即响应| A1 A1 -.->|连接关闭| B1 end subgraph 长轮询 A2[客户端] -->|请求| B2[服务端] B2 -.->|hold 30秒| B2 B2 -->|有变更或超时| A2 A2 -.->|等2秒| A2 A2 -->|下一轮请求| B2 end subgraph 长连接_WebSocket A3[客户端] <-->|持续保持| B3[服务端] B3 -.->|任何时候都可推送| A3 end

短连接:

  • 发请求,立刻响应,连接关闭(或放回连接池)
  • 就是普通的 REST API

长连接(WebSocket/SSE):

  • 建立连接后一直保持
  • 服务端可以随时推送数据
  • 适合高频双向通信(聊天、游戏、股票行情)

长轮询:

  • 还是 HTTP 请求,但服务端会 hold 住一段时间
  • 本质上是"长时间的请求",不是"长时间的连接"
  • 适合低频推送(配置变更、通知)

Nacos 为什么用长轮询而不是 WebSocket?我猜测主要是:

  1. 配置变更频率低,不需要 WebSocket 的高频能力
  2. HTTP 兼容性好,容易穿透各种代理和网关
  3. 实现简单,服务端压力小

动手搞一个简化版

我们这个项目叫 surfing-nacos-refresh,目标是实现:

  • 客户端通过 HTTP 长轮询监听配置变更
  • 服务端 hold 住请求,有变更时立刻返回
  • 客户端拿到变更信号后,拉取最新配置
  • 自动刷新 Spring 的 Environment
  • 支持 @Value@ConfigurationProperties 热更新

整体架构

graph LR A[客户端应用] -->|长轮询监听| B[配置服务器] A -->|拉取配置| B A -->|更新Environment| A A -->|刷新Bean| A B -->|返回变更信号| A B -->|返回配置内容| A

服务端:怎么 hold 住请求

服务端最核心的就是这个 /listener 接口,客户端会不断发请求过来,我们要把请求挂起,等配置变更了再返回。

java 复制代码
@PostMapping("/v1/cs/configs/listener")
public void listener(
    @RequestParam("Listening-Configs") String listeningConfigs,
    HttpServletRequest request,
    HttpServletResponse response) throws IOException {
    
    log.info("[Listener] 收到订阅: {}", listeningConfigs);
    
    // 解析订阅串:dataId^2^group^2^md5^1
    List<ConfigKey> keys = parseListeningConfigs(listeningConfigs);
    
    // 先比较 md5,看看有没有变更
    List<ConfigKey> changedKeys = new ArrayList<>();
    for (ConfigKey key : keys) {
        String currentMd5 = getConfigMd5(key.getDataId(), key.getGroup());
        if (!Objects.equals(currentMd5, key.getMd5())) {
            changedKeys.add(key);
        }
    }
    
    // 有变更,立刻返回
    if (!changedKeys.isEmpty()) {
        log.info("[Listener] 检测到变更,立即返回: {}", changedKeys);
        String result = buildChangedResponse(changedKeys);
        response.getWriter().write(result);
        return;
    }
    
    // 没变更,挂起请求
    log.info("[Listener] 无变更,挂起请求");
    AsyncContext asyncContext = request.startAsync();
    asyncContext.setTimeout(0);  // 禁用容器默认超时
    
    addLongPollingClient(keys, asyncContext);
}

这里有几个关键点:

1. 先比较后挂起

服务端不是盲目挂起所有请求,而是先对比客户端发过来的 md5 和当前配置的 md5。如果不一样,说明客户端的配置已经过期了,立刻返回变更信号。

只有 md5 一致的情况下,才会挂起请求。

2. AsyncContext

这是 Servlet 3.0 提供的异步处理能力。普通的 Servlet 请求会占用一个工作线程,如果要 hold 30 秒,那这个线程就得等 30 秒。用了 AsyncContext 之后,工作线程可以释放,请求被转移到异步线程池处理。

java 复制代码
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);  // 这个很重要!

setTimeout(0) 是禁用容器的默认超时机制,我们要用自己的超时调度。

3. 挂起的细节

java 复制代码
private void addLongPollingClient(List<ConfigKey> keys, AsyncContext asyncContext) {
    ClientSubscription sub = new ClientSubscription(keys, asyncContext);
    
    // 加到订阅列表
    for (ConfigKey key : keys) {
        subscribers.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(sub);
    }
    
    // 定时 29.5 秒后超时返回
    ScheduledFuture<?> timeoutTask = scheduler.schedule(() -> {
        try {
            log.info("[LongPolling] 超时,返回空字符串");
            asyncContext.getResponse().getWriter().write("");
            asyncContext.complete();
        } catch (Exception e) {
            log.error("[LongPolling] 超时处理失败", e);
        } finally {
            removeSubscriber(sub);  // 别忘了清理!
        }
    }, 29500, TimeUnit.MILLISECONDS);
    
    sub.setTimeoutTask(timeoutTask);
}

这里用了一个 ScheduledExecutorService 来管理超时。29.5 秒后,如果配置还没变,就返回空字符串,然后 complete 这个 AsyncContext。

4. 配置发布时的通知

当有人调用 /configs 接口发布新配置时,我们要立刻通知所有在等待的客户端:

java 复制代码
@PostMapping("/v1/cs/configs")
public String publishConfig(
    @RequestParam("dataId") String dataId,
    @RequestParam("group") String group,
    @RequestParam("content") String content) {
    
    // 保存配置
    saveConfig(dataId, group, content);
    
    // 通知所有订阅者
    notifyConfigChange(dataId, group);
    
    return "success";
}

private void notifyConfigChange(String dataId, String group) {
    ConfigKey key = new ConfigKey(dataId, group);
    List<ClientSubscription> subs = subscribers.get(key);
    
    if (subs != null) {
        log.info("[LongPolling] 通知 {} 个订阅者", subs.size());
        
        for (ClientSubscription sub : subs) {
            // 取消超时任务
            sub.cancelTimeout();
            
            // 返回变更信号
            try {
                String result = dataId + "^2^" + group + "^1";
                sub.getAsyncContext().getResponse().getWriter().write(result);
                sub.getAsyncContext().complete();
            } catch (Exception e) {
                log.error("[LongPolling] 通知失败", e);
            }
            
            // 清理订阅
            removeSubscriber(sub);
        }
        
        subscribers.remove(key);
    }
}

5. 订阅清理(重要!)

这个地方我当时踩了个坑。如果超时或者通知后不清理订阅,subscribers 这个 Map 会越来越大,而且会有很多已经 complete 的 AsyncContext 留在里面,再次通知时就会报错。

所以无论是超时还是通知,都要记得清理:

java 复制代码
private void removeSubscriber(ClientSubscription sub) {
    for (ConfigKey key : sub.getKeys()) {
        List<ClientSubscription> subs = subscribers.get(key);
        if (subs != null) {
            subs.remove(sub);
            if (subs.isEmpty()) {
                subscribers.remove(key);
            }
        }
    }
}

客户端:怎么发起长轮询

客户端这边用了一个定时器,每隔一段时间(retryInterval)就发起一次长轮询请求:

java 复制代码
public void start() {
    // 使用定时器,每 retryInterval 秒执行一次长轮询
    executor.scheduleWithFixedDelay(
        new LongPollingRunnable(), 
        0,  // 立即开始
        retryInterval,  // 间隔时间(默认2秒)
        TimeUnit.SECONDS
    );
}

class LongPollingRunnable implements Runnable {
    @Override
    public void run() {
        try {
            // 构造订阅串
            String listeningConfigs = buildListeningConfigs();
            System.out.println("[LongPolling] request Listening-Configs=" + listeningConfigs);
            
            RequestBody body = new FormBody.Builder()
                .add("Listening-Configs", listeningConfigs)
                .add("timeout", String.valueOf(timeout * 1000))
                .build();
                
            Request request = new Request.Builder()
                .url(serverAddr + "/v1/cs/configs/listener")
                .post(body)
                .build();
            
            // 这里会阻塞最多 30 秒
            try (Response resp = httpClient.newCall(request).execute()) {
                if (resp.isSuccessful()) {
                    String content = resp.body() != null ? resp.body().string() : "";
                    
                    if (StringUtils.isNotEmpty(content)) {
                        // 有变更
                        System.out.println("[LongPolling] 检测到变更: " + content);
                        List<ConfigKey> changedKeys = parseChangedConfigs(content);
                        for (ConfigKey key : changedKeys) {
                            CacheData cd = getCacheData(key.dataId, key.group);
                            if (cd != null) {
                                fetchFromServer(cd);  // 拉取最新配置
                            }
                        }
                    }
                    // 无变更的话,定时器会在 retryInterval 秒后自动触发下一轮
                }
            }
        } catch (Exception e) {
            // 出错了,定时器也会在 retryInterval 秒后重试
            System.err.println("[LongPolling] 请求失败: " + e.getMessage());
        }
    }
}

这里有几个时间上的设计:

sequenceDiagram participant Timer as 定时器 participant Client as 客户端 participant Server as 服务端 Note over Timer: retryInterval=2秒 Timer->>Client: 触发长轮询任务 Client->>Server: POST /listener (timeout=30s) Note over Server: hold 请求 alt 29.5s内有变更 Note over Server: 立即返回变更信号 Server->>Client: dataId^2^group^1 Client->>Server: GET /configs Server->>Client: 配置内容 Note over Client: 处理完成 Note over Timer: 等待2秒 Timer->>Client: 触发下一轮 else 29.5s超时 Server->>Client: 空字符串 Note over Client: 收到响应 Note over Timer: 等待2秒 Timer->>Client: 触发下一轮 end

关键的时间关系:

  • 服务端挂起超时:29.5 秒
  • 客户端请求超时:30 秒(OkHttp readTimeout)
  • 定时器触发间隔:2 秒(retryInterval)

这个设计很巧妙:

1. 为什么服务端是 29.5 秒?

因为要比客户端的 30 秒小一点,确保服务端先返回。如果服务端超时设置成 30 秒或更大,客户端可能先超时断开连接,服务端还在傻等。

2. 为什么用定时器而不是 while 循环?

用定时器(scheduleWithFixedDelay)的好处是:

  • 每次任务执行完(包括30秒的阻塞等待),等固定间隔(2秒)后才开始下一轮
  • 如果某次请求耗时长(比如网络慢),不会立刻发下一个请求
  • 更容易控制并发,不会因为异常导致疯狂重试

对比 while 循环:

java 复制代码
// while 循环需要手动 sleep
while (running) {
    doLongPolling();  // 可能耗时30秒
    Thread.sleep(2000);  // 再等2秒
}

// 定时器自动处理
executor.scheduleWithFixedDelay(() -> {
    doLongPolling();  // 可能耗时30秒
    // 任务结束后,定时器自动等2秒再触发下一轮
}, 0, 2, TimeUnit.SECONDS);

3. 为什么间隔是 2 秒?

这是个缓冲时间,避免客户端连续不断地发请求。实际的循环是:

  • 发请求 → 服务端 hold 29.5秒 → 返回 → 定时器等2秒 → 下一轮

所以如果配置一直不变,客户端的请求频率是:29.5秒 + 2秒 = 31.5秒一轮

拉取配置

客户端收到变更信号后,要再发一个 GET 请求拉取完整配置:

java 复制代码
private void handleConfigChange(String changedConfigs) {
    // 解析变更的 dataId 和 group
    String[] parts = changedConfigs.split("\\^1");
    for (String changed : parts) {
        String[] keyValue = changed.split("\\^2");
        if (keyValue.length >= 2) {
            String dataId = keyValue[0];
            String group = keyValue[1];
            
            // 拉取最新配置
            String newContent = fetchConfig(dataId, group);
            
            // 更新本地缓存
            updateLocalCache(dataId, group, newContent);
            
            // 触发刷新
            receiveConfigInfo(newContent);
        }
    }
}

private String fetchConfig(String dataId, String group) throws IOException {
    Request request = new Request.Builder()
        .url(serverAddr + "/v1/cs/configs?dataId=" + dataId + "&group=" + group)
        .get()
        .build();
    
    try (Response resp = httpClient.newCall(request).execute()) {
        return resp.body().string();
    }
}

这里是两阶段拉取:

  1. 长轮询只返回"哪些配置变了"
  2. 客户端再根据这个信号,去拉取具体内容

为什么不直接在长轮询里返回配置内容?因为:

  • 长轮询的响应要尽量小,快速返回
  • 配置内容可能很大,放在长轮询响应里会阻塞其他订阅者的通知
  • 分离变更信号和内容拉取,职责更清晰

刷新 Spring Environment

拿到新配置后,要更新 Spring 的 Environment,然后触发刷新事件:

java 复制代码
private void receiveConfigInfo(String content) {
    try {
        // 解析配置(支持 YAML 和 Properties)
        Properties props = parseConfig(content);
        
        // 更新 Environment
        ConfigurableEnvironment env = applicationContext.getEnvironment();
        MutablePropertySources sources = env.getPropertySources();
        
        // 创建新的 PropertySource
        PropertySource<?> propertySource = new PropertiesPropertySource(
            "nacos-config", props
        );
        
        // 替换或添加
        if (sources.contains("nacos-config")) {
            sources.replace("nacos-config", propertySource);
        } else {
            sources.addFirst(propertySource);
        }
        
        // 发布事件
        Set<String> keys = props.stringPropertyNames();
        applicationContext.publishEvent(new EnvironmentChangeEvent(keys));
        
        log.info("[Refresh] Environment 已更新,变更的 key: {}", keys);
    } catch (Exception e) {
        log.error("[Refresh] 配置刷新失败", e);
    }
}

EnvironmentChangeEvent 是 Spring Cloud 提供的,@RefreshScope 的 Bean 会监听这个事件,然后销毁旧实例,等下次使用时重新创建。

支持 @Value 刷新(我们加的)

但是光靠 @RefreshScope 还不够,我们希望普通的 @Value 也能刷新。所以我们加了一个 @Refresh 注解:

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Refresh {
}

然后写了一个监听器,专门处理带 @Refresh 注解的类:

java 复制代码
@Component
public class ValueRefreshListener {
    
    @EventListener
    public void onEnvironmentChange(EnvironmentChangeEvent event) {
        // 找到所有带 @Refresh 的 Bean
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(Refresh.class);
        
        for (Object bean : beans.values()) {
            refreshValueFields(bean, event.getKeys());
        }
    }
    
    private void refreshValueFields(Object bean, Set<String> changedKeys) {
        Class<?> clazz = bean.getClass();
        
        // 遍历所有字段
        for (Field field : clazz.getDeclaredFields()) {
            Value valueAnnotation = field.getAnnotation(Value.class);
            if (valueAnnotation != null) {
                String placeholder = valueAnnotation.value();
                String key = extractKey(placeholder);  // 提取 ${xxx} 里的 key
                
                if (changedKeys.contains(key)) {
                    // 从 Environment 获取最新值
                    Object newValue = resolveValue(placeholder, field.getType());
                    
                    // 反射设置
                    field.setAccessible(true);
                    try {
                        field.set(bean, newValue);
                        log.info("[Refresh] 更新字段: {}.{} = {}", 
                            clazz.getSimpleName(), field.getName(), newValue);
                    } catch (IllegalAccessException e) {
                        log.error("[Refresh] 字段更新失败", e);
                    }
                }
            }
        }
    }
}

这样一来,只要在类上加个 @Refresh 注解,里面的 @Value 字段就能自动刷新了:

java 复制代码
@Component
@Refresh  // 加这个!
public class FeatureConfig {
    @Value("${feature.new-algorithm:false}")
    private boolean useNewAlgorithm;
    
    @Value("${database.pool-size:10}")
    private int poolSize;
}

对比 Nacos,我们做了什么

保留的:

  • HTTP 长轮询机制(核心)
  • 两阶段拉取(变更信号 + 配置内容)
  • 先比较后挂起的逻辑
  • 超时时间设计(29.5s < 30s)

简化的:

  • 没有做服务发现(Nacos 2.x 用 gRPC)
  • 没有做配置加密、权限控制
  • 没有做灰度发布
  • 没有做多数据中心同步
  • 配置存储直接用内存(Nacos 用数据库)

增强的:

  • 加了 @Refresh 注解支持 @Value 刷新
  • 这块 Nacos 本身是通过 @RefreshScope 实现的,我们提供了另一种方案

踩过的坑

坑1:订阅清理

前面提到过,如果不清理订阅,subscribers 会越来越大。而且 AsyncContext complete 之后再调用会报错。我们的做法是:

  • 超时时清理
  • 通知时清理
  • 异常时也要在 finally 里清理

坑2:超时时间

一开始我把服务端超时设成 30 秒,客户端也是 30 秒,结果经常出现客户端超时断开,服务端还在等。后来改成 29.5 秒就好了。

而且网关的超时要设得更大,我们用的 Nginx,proxy_read_timeout 设成了 60 秒,确保不会被中间层断开。

坑3:线程池

AsyncContext 会用到异步线程池,如果配置不当,高并发时会出现拒绝。我们用的是 Tomcat,在 application.yml 里加了:

yaml 复制代码
server:
  tomcat:
    threads:
      max: 200
      min-spare: 10
    max-connections: 10000
    accept-count: 100

坑4:Environment 更新时机

有一次我们发现配置更新了,但 Bean 里的值还是旧的。后来发现是事件监听的顺序问题。EnvironmentChangeEventRefreshScopeRefreshedEvent 可能并发触发,如果你在两个事件里都做了操作,可能会有时序问题。

我们后来统一只监听 EnvironmentChangeEvent,问题就解决了。

整个流程串起来

最后画个完整的流程图,把服务端和客户端的交互都串起来:

sequenceDiagram participant App as 客户端应用 participant Long as 长轮询线程 participant Server as 配置服务器 participant Admin as 管理员 Note over App: 应用启动 App->>Long: 启动长轮询 loop 长轮询循环 Long->>Server: POST /listener (timeout=30s) Note over Server: 先比较 md5 alt md5 不一致 Server->>Long: 立即返回变更信号 else md5 一致 Note over Server: 挂起请求 (29.5s) par 等待期间 Admin->>Server: POST /configs (发布新配置) Server->>Server: 保存配置 Server->>Server: 取消超时任务 Server->>Long: 返回变更信号 and 如果超时 Note over Server: 29.5s 到 Server->>Long: 返回空字符串 end end alt 有变更 Long->>Server: GET /configs (拉取配置) Server->>Long: 返回配置内容 Long->>App: 更新 Environment App->>App: 发布 EnvironmentChangeEvent App->>App: 刷新 @RefreshScope Bean App->>App: 刷新 @Refresh 字段 Note over Long: 等待 2 秒 else 无变更 Note over Long: 等待 2 秒 end end

性能和稳定性

我们在本地跑了压测,模拟 100 个客户端同时长轮询,服务端没啥压力。因为请求被挂起后,工作线程就释放了,不会占用资源。

配置发布时的通知也很快,基本上是毫秒级。我们测试了同时通知 100 个客户端,耗时不到 50ms。

当然这只是个 Demo,生产环境还要考虑:

  • 配置持久化(数据库或文件)
  • 多实例部署时的配置同步
  • 客户端断线重连
  • 配置版本管理
  • 权限控制

但核心的长轮询机制已经跑通了,这是最重要的。

几个还没搞透的地方

关于泛型刷新:

前面那篇文章里提到的嵌套 Map 刷新问题,我在这个 Demo 里还没完全解决。如果你的配置类有复杂泛型,可能还是需要手动监听 EnvironmentChangeEvent,用 ResolvableType 绑定。

这块我还在研究,可能跟 Spring Boot 版本有关,也可能是 Binder 的实现细节。如果有大佬知道更好的方案,欢迎指点。

关于 @Async 方法:

如果你的服务里有 @Async 异步方法,或者自己创建的线程,这些地方拿到的配置对象可能不是最新的。因为 @RefreshScope 的代理只在 Spring 容器管理的调用链路上生效。

解决办法是在异步方法里重新获取 Bean,或者把配置值作为参数传进去。但这个我还没找到特别优雅的方案。

关于配置回滚:

如果新配置有问题,导致应用启动失败或者运行异常,怎么快速回滚?Nacos 有配置历史版本管理,我们这个 Demo 还没做。

这块可以考虑保留最近 N 个版本的配置,出问题时快速切回去。

写在最后

周五晚上下班回家就开始撸代码,搞到凌晨1点多。周六早上6点又爬起来继续干,前前后后投入了差不多10个小时。

但收获挺大的:

  1. 彻底搞懂了 Nacos 的长轮询机制
  2. 理解了 @RefreshScope 的原理
  3. 知道了配置刷新的完整链路
  4. 还顺便学了 AsyncContext 的用法

最重要的是,这些知识不是看视频看文章能学到的。只有自己动手写一遍,调试一遍,踩坑一遍,才能真正理解。

之前看 Nacos 源码,总觉得代码太多,看着看着就迷糊了。现在自己写了个简化版,再去看源码,就清晰多了。哪些是核心逻辑,哪些是边界处理,一目了然。

项目地址

GitHub: surfing-nacos-refresh

完整项目包含:

  • 服务端实现(配置存储、长轮询、推送通知)
  • 客户端实现(长轮询、配置拉取、Environment刷新)
  • Spring Boot Starter(开箱即用)
  • 可运行的 Demo(本地启动就能测试)

代码已经完全开源,clone 下来就能跑。总共也就几百行代码,但该有的功能都有了。

为什么选择开源?

这是个学习项目,目的是理解原理,不是造轮子。既然是学习,就应该分享出来,让更多人受益。

生产环境还是建议用官方的 Nacos,人家经过了大规模验证,功能更完善。

如果觉得有帮助:

  • 文章点个赞、收藏一下
  • 有问题欢迎提 issue 或者评论区交流

周末写代码+写文章确实挺累的,被老婆嫌弃了好几次"又在敲代码"。但看到能帮到大家,也值了。

下一步计划

下一篇文章打算写"Nacos 配置中心的常见面试问题",把这次学到的知识点整理成面试题的形式,比如:

  • 长轮询和长连接的区别?
  • 为什么服务端超时是 29.5 秒?
  • 配置刷新的完整流程是怎样的?
  • @RefreshScope 的原理是什么?
  • 如何保证配置刷新的线程安全?

敬请期待。


最后再问一句:你们项目里用的配置中心是什么?遇到过配置刷新的问题吗?

欢迎在评论区分享你的经验,我们一起交流学习。如果有什么疑问,也可以留言,我都会回复的。

周末愉快!

相关推荐
listening7776 小时前
HarmonyOS 6.1 混沌工程实战:从“故障免疫”到“韧性架构”
华为·架构·harmonyos
晚风吹长发6 小时前
Docker使用——Docker容器及相关命令
linux·运维·服务器·docker·容器·架构
电子科技圈6 小时前
先进封装、芯粒架构和3D集成——先进异构集成亟需兼具标准化与定制化能力的互联及总线IP解决方案
tcp/ip·设计模式·架构·软件构建·代码规范·设计规范
止语Lab7 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活7 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困7 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn7 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
AI小白Lin7 小时前
33 个 AI 专家全票通过?那问题才刚开始
人工智能·架构
用户298698530147 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908407 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端