JDK 21 规模化落地实践

JDK 8 撑起了货拉拉一整代微服务,上千个在线服务至今还跑在上面。升到 JDK 21,运行时、Spring 和一批中间件都要跟着换代。改 java.version 只要一行,难的是编译通过以后,很多依赖的行为已经悄悄变了。这次我们让框架先把兼容成本扛下来,重复的代码改造交给 AI 批量完成,业务工程切 parent、按检查单收尾。底座先动,业务后动。

一、背景

升级收益

收益 说明
生命周期 回到主流 LTS,安全补丁和根证书更新都有长期保障。Spring Boot 2.x 早已停止开源维护,留在老栈上,漏洞只能自己打补丁。
生态 开源框架、云厂商 JDK、社区方案和 AI 编程工具,都已把 JDK 21 与 Spring 新一代当作默认基线。新能力、示例和后续 LTS 沿这条线发布,外部经验可以直接落进工程,不必再为老栈单独维持分支。
资源效率 JVM 能正确识别容器的内存和 CPU 配额,不再按宿主机估算。同等业务量下,线上实测 CPU 降 10%、内存降 50%。

升级挑战

升到 JDK 21 几乎一定会带出整栈换代:Spring Boot 3、Jakarta EE、新一代连接器和序列化库,还有一批内部二方包。如果只升了 JDK,传递依赖和运行时语义没有一起动,编译往往照样能过,类、注解和序列化要到启动或第一个请求时才对不上。

更隐蔽的问题要到发布和灰度时才出现。自动配置从 spring.factories 换成了 AutoConfiguration.imports,只写了前者的自定义 Starter 会静默失效;连接器、RPC 和监控组件的默认行为也和 2.x 不同,延迟、指标或链路会在放量时慢慢偏离。线程模型、容器里的 CPU 和内存可见性也一样,照搬旧参数,要等流量切过来才暴露。

升级思路

升级方式 各工程自己升 框架先动
版本管理 各自在 POM 里对版本 BOM 统一收口,业务不写版本号
Jakarta / 中间件 / RPC 每个工程各改一遍 框架里先做完
代码改造 样板代码手改,兼容问题靠人记 AI 批量改写,并按已知兼容问题清单扫描
业务要做的 逐个依赖排雷 切 parent,按检查单对齐基线

公司的 Java 服务统一建在 JAF 上,大部分兼容工作可以由框架替业务先做掉。整体思路:

  • 框架收口:版本统一收进 BOM,Jakarta 迁移、中间件和 RPC 适配在框架里先做完,详见第二章

  • AI 辅助:javax → jakarta 这类重复改动交给 AI 批量改写,同时按已确认的兼容问题清单扫描代码,人工负责验收

  • 发布可回滚:按 STG → PRE → 生产推进,核心服务先灰度放量,出问题一键回到老版本

业务侧切完 parent,还要按第二章末尾的检查单,把 javax、自定义 Starter、内部组件和 Lombok 对齐到同一基线,否则框架已经解决的问题会在业务工程里重新冒出来。

二、框架换代:底座先动,业务后动

上千个服务如果各自升级,很难收尾,所以这次换代主要由框架层来扛。框架按大版本分代:2.x 对应 JDK 8 + Spring Boot 2,3.x 对应 JDK 21 + Spring Boot 3.5 + Jakarta。JAF、Hermes、Monitor 三个核心仓库一起升级、版本对齐,业务工程只声明依赖,不写版本号。

组件 定位 这次换代做什么
lalaframework(JAF) 分布式应用框架:配置、缓存、ORM、消息、SOA、治理等 Starter 体系 编译基线升 JDK 21,javax 全量切 jakarta,用 BOM 收口依赖
hll-hermes 自研 SOA/RPC 框架:JSON-RPC / gRPC、Consul 服务发现、路由与治理 RPC 对齐 Spring 6,与 JAF 同代绑定
lala-jaf-monitor 监控组件:Prometheus 指标 + SkyWalking 链路,agent / 非 agent 两种接入 指标与链路换新引擎,灰度期间观测不断档

Hermes 与 JAF 版本强绑定(hermes.version = lala.release.version),不会出现跨代混用。2.x 和 3.x 两条线并行维护:新项目直接用 3.x 脚手架,存量服务按自己的节奏迁移,不必一次性切换;2.x 上验证过的能力(Kafka、exemplar 等)也会回灌到 3.x。

依赖整组抬升

框架升级的第一步,是把依赖整组抬上去,由 BOM 统一锁定版本,中间做了很多依赖的兼容适配。

重点关注两类问题:

Spring 管理的组件版本是否适配公司内部组件

举例:

Redis: 公司内部Datamesh/redis版本是否适配,协议/命令等。

其他如Mysql/ES等,都需适配公司内部的服务端版本。

组件本身是否有Bug

举例:

Spring 6.2.7管理的httpcore5:5.4版本存在bug,默认设置的tcpNoDelay:false失效,导致大包 RPC 延迟约 +40ms,升级到5.4.2版本才能解决。

主要依赖的版本对照如下,业务工程不要在 POM 里再覆盖这些版本:

依赖 2.x 3.x
JDK 8 21
Spring Boot 2.2.6 3.5.0
Spring Framework 5.2.5 6.2.7
Servlet API javax 4.0 jakarta 6.0
Micrometer 1.5.x 1.15.0
Fastjson 1.2.x 2.0.x
MyBatis / MyBatis-Plus 3.5.4 / 3.3.0 3.5.19 / 3.5.12
MySQL Connector 5.1.48 9.2.0
gRPC 1.41.1 1.73.0
Jackson 2.10.3 2.19.0
Httpclient5 4.x 5.x

内部配套组件必须和框架同代,不支持 2.x 框架和 3.x 组件混用。升级评审时,先看依赖树有没有冲突,再看业务代码 diff。

JAF:中间件封装换新底座

  • Jakarta 迁移:javax.* 全量切到 jakarta.*,编译基线升到 JDK 21,版本管理改为 revision + flatten-maven-plugin

  • 自动配置注册机制切换:META-INF/spring.factories 替换为 AutoConfiguration.imports(约 42 个文件)。自定义 Starter 如果只写了 spring.factories,在 Boot 3 下直接不生效

  • MyBatis 分页重构:JSqlParser 大版本升级,自研 pagehelper parser 整体重写,删掉约 1100 行旧实现

  • 启动相关:健康检查默认关闭,避免探活拖垮启动;预热体系增强,按组件配置超时与重试

  • 3.x 持续演进:3.1 动态线程池支持虚拟线程与上下文封装,3.2 引入 JetCache,3.3+ 落地 OceanBase / OBKV 与 LLM KMS 加密

Hermes:RPC 底座的 Spring 6 适配

  • 去掉外部依赖:移除 alibaba spring-context-support,自研 BeanRegistrar 与上下文事件监听,注解处理收归框架内部

  • Spring 6 API 适配:HandlerInterceptorAdapter 被删,改为直接实现 HandlerInterceptor;InstantiationAwareBeanPostProcessorAdapter 改为直接实现接口,postProcessPropertyValues 换成 postProcessProperties

  • 依赖升级:gRPC 1.41 → 1.73、Jackson 2.10 → 2.19、Resilience4j 1.7 → 2.3、jjwt 0.11 → 0.12(parserBuilder 换成 parser)

  • 老包名兼容:hermes-compatible 保留 cn.lalaframework.soa 旧包名,老业务的 SOA API 不用改写就能迁到 3.x

升级后踩到的坑:HttpClient5 升级后大包 RPC 延迟约 +40ms,原因是 Nagle 与 Delayed ACK 叠加。显式打开 tcpNoDelay,并把 httpcore5 升到 5.4.2 才彻底修复;连接池默认策略也按升级前的行为对齐了回去。

Monitor:指标与链路换新引擎

  • Micrometer 1.5 → 1.15:Prometheus 包整体迁移:io.micrometer.prometheus → io.micrometer.prometheusmetrics,CollectorRegistry 换成 PrometheusRegistry,expire 指标体系重写

  • ByteBuddy 1.10 → 1.15:agent 的 Transformer 适配新的 ProtectionDomain 签名,清掉 sun.security 等 JDK 内部 API 调用

  • 插件换代:删除 SOA v1.4 旧插件,新增 Spring MVC 6.x、RestTemplate 6.x、WebClient、Tomcat 10.x 插件,链路能力对齐新栈

  • 能力回灌:Kafka 3.x 支持与 exemplar 能力从 2.x 回灌,灰度期间观测不断档

业务升级检查单

框架做完公共部分,业务工程要改的就收敛到下面五项。改完之后回归要测什么,见第三章末尾的回归检查单。

  • parent 切到 lala-boot-parent 3.x,删掉 POM 里所有手写版本号

  • javax → jakarta 全量替换,AI 辅助改造 + 人工验收

  • 自定义 Starter 确认已改用 AutoConfiguration.imports

  • 内部组件(hll-meta / config-client / prometheus / whitzard / hermes)确认与框架同代

  • 编译基线切 JDK 21,Lombok 等编译期组件同步对齐

三、踩坑实录:编译是绿的,语义已经变了

规模化升级里最耗时间的,是编译全绿、语义却已经变了的那类问题。下面 12 个坑都是这次升级中真实踩过的,分依赖与契约、发布与资源模型两类,每条按现象、原因、做法写。章末附了一份回归检查单,可以直接对照使用。

依赖与契约:接口没变,行为变了

01~03 是依赖和构建问题,旧版本、旧封装在新栈上直接失效;04~08 出在契约和语义上,编译器一个都拦不住,只能靠回归覆盖。

发布与资源模型:灰度窗口里才会爆的坑

09~12 和发布方式、机器规格有关:有的只在灰度窗口里出现,有的要到生产规格的机器上才能复现,线下环境很难提前发现。

回归检查单

回归时逐项对照,括号里是对应的坑:

  • 依赖树里 javax 与 jakarta 不并存,业务 POM 不覆盖 BOM 版本,Lombok 在 1.18.30 及以上(01、02)

  • 分页走官方拦截器,定时任务升级后确认节点还在、能正常触发(03)

  • 对外契约显式声明字段名,校验放在服务端边界,失败路径做了契约测试(04、06、08)

  • 时间字段回归覆盖「5.x 写入、9.x 读出」的混合数据,反序列化用真实线上报文回放(05、06)

  • 调用方 path 里没有 //,确需兼容的用 UrlPathHelper 显式放宽(07)

  • Kafka 消费组整组切换,或接受短窗口内只有旧实例消费;生产默认关闭远程调试(09、10)

  • 打开钉住跟踪,热路径没有 Pinning;至少一轮回归跑在 3 核及以上的机器上(11、12)

四、特性应用:虚拟线程与语言新特性

虚拟线程:先选场景

平台线程和 OS 线程一对一,创建和切换成本都高。虚拟线程由 JVM 调度,阻塞在 HTTP、JDBC、Redis、RPC 上时会从载体线程卸下,载体转去执行其他虚拟线程。所以它适合大量等待、少量计算的同步代码。IO 并发靠 Semaphore 或连接池限流就行,不必为了限流去配一个很小的平台线程池。

这几类场景继续用平台线程:CPU 密集的加解密、大 JSON 序列化,虚拟线程不会让计算变快;定时任务、XXL-JOB 和 Kafka 消费的线程由各自框架管理,并发也受任务数或分区数限制,换成虚拟线程收益不大。

Tomcat 打开 spring.threads.virtual.enabled 会把整个请求切到虚拟线程,打开前先确认 Filter / 拦截器没有在 synchronized 里做阻塞 IO。请求本身不会断 Trace,只有在请求里再拆出新的虚拟线程时,才需要走 Helper。

JAF 的两种接入方式

VirtualThreadContextHelper

父线程把任务交给一条新的虚拟线程时,ThreadLocal 不会自动传递;同一条虚拟线程内阻塞 IO、换载体,则不需要再包一层。Helper 在提交前依次做两件事:

  1. RunnableWrapper.of 捕获 SkyWalking Trace

  2. ContextAwareRunnable.of 带上业务 ContextHeaders

runVirtual

对应 Thread.startVirtualThread

newVirtualThreadExecutor

对应每任务一条新 VT 的无界执行器(ThreadPerTaskExecutor)

Java 复制代码
// 原生(无 Trace 要求的独立脚本可用)
try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
    ex.submit(() -> doWork());
}

// 框架推荐(业务链路必用)
VirtualThreadContextHelper.runVirtual(() -> doWork());
CompletableFuture<String> f = VirtualThreadContextHelper.supplyVirtual(() -> loadData());
ExecutorService ex = VirtualThreadContextHelper.newVirtualThreadExecutor("biz-virtual-");

动态线程池

打开 virtualThread=true 后,动态线程池只换创建线程的方式,执行器仍然是 ThreadPoolExecutor,核心数、最大数、队列、拒绝策略都有效,创建出来的 Worker 全部是虚拟线程。调度池 DtpScheduledExecutor 会忽略这个开关。

Java 复制代码
@Bean
public ThreadPoolExecutor virtualExecutor() {
    return ThreadPoolBuilder.newBuilder()
            .threadPoolName("virtualExecutor")
            .corePoolSize(20)
            .virtualThread(true)
            .maximumPoolSize(100)
            .queueCapacity(2048)
            .buildDynamic();
}

它和 newVirtualThreadPerTaskExecutor() 不一样:后者每提交一个任务就新建一条 VT,没有队列,也没有 core/max。DTP 的 core/max 限制的是 Worker 虚拟线程数,不是载体。载体由 JVM 按 CPU 核数按需提供,默认上限约为 max(核数, 256),和 Tomcat、Helper 共用。IO 场景可以调大 DTP 的 max 来提高并发。

语言特性

JDK 21 里可以马上用的,是已经转正的模式匹配、Record 拆字段、有序集合和文本块。它们不影响性能,好处是少写强制转换和样板代码,配合 sealed 接口还能在编译期发现漏掉的分支。

可以进生产

模式匹配、Record 拆字段、有序集合、文本块

生产禁用

String Templates、Structured Concurrency 等还没转正的特性,不要加 --enable-preview

模式匹配:少一次强制转换

instanceof 判断类型时可以直接带上变量,从 JDK 16 起就能用;switch 按类型分支在 JDK 21 转正。两者都能去掉强制转换和重复判空。

Java 复制代码
if (factory instanceof NamedThreadFactory named) {
    return named.getNamePrefix();
}

if (dataSource instanceof ItemDataSource itemDataSource) {
    return itemDataSource.getRealDataSource();
}

switch 适合同一个入口、多种类型的分发。对 sealed 接口做 switch 且不写 default 时,漏掉分支会直接编译报错。有两个容易踩的点:

  • null 要单独写 case null,否则空值会直接 NPE

  • 带 when 的条件要写在更宽泛的 case 前面,否则会被前面的 case 覆盖,编译直接报错

Java 复制代码
static String describe(Object value) {
    return switch (value) {
        case null -> "empty";
        case String s when s.isBlank() -> "blank";
        case String s -> "text:" + s.length();
        case Integer i -> "int:" + i;
        default -> value.getClass().getSimpleName();
    };
}

Record:只承载数据,别当 Bean 用

适合

只读的数据对象、缓存 Key、几个字段拼成的路由 Key

继续用 class

MyBatis 实体、带 setter 的配置、Spring Bean

和 switch 一起用时,可以直接拆出里面的字段:

Java 复制代码
public record RouteKey(String tenantId, String table) {}

static String shard(Object key) {
    return switch (key) {
        case RouteKey(String tenant, String table) -> tenant + ":" + table;
        case String s -> s;
        default -> "default";
    };
}

有序集合:官方的 getFirst / getLast

JDK 21 给 List、LinkedHashSet 这类有序集合补了统一的 getFirst() / getLast(),不用再写 get(0) 和 get(size - 1);LinkedHashMap 对应的是 firstEntry() / lastEntry()。空 List 调 getFirst() 会抛 NoSuchElementException,调用前先判空。reversed() 返回的是反向视图,不是拷贝,需要快照时用 new ArrayList<>(list.reversed())。

Java 复制代码
List<String> steps = new ArrayList<>(List.of("auth", "route", "invoke"));
steps.addFirst("warmup");
String last = steps.getLast();
List<String> reversed = steps.reversed(); // 视图,不是拷贝

LinkedHashMap<String, Integer> order = new LinkedHashMap<>();
order.putLast("tail", 1);
Map.Entry<String, Integer> first = order.firstEntry();

文本块:样例和测试用,别拼运行时 SQL

文本块(""")适合单测里的 JSON 样例和错误报文模板。运行期拼接带用户输入的 SQL 仍然有注入风险,这点和以前一样。

Java 复制代码
String payload = """
        {"orderId": "%s", "status": "CREATED"}
        """.formatted(orderId);

五、运行时治理:让 JVM 看见真实的容器

升级带来的运行时收益,要靠三件事落到线上:按延迟要求选 GC,让 JVM 读到容器真实的内存和 CPU,排障先看聚合指标、不再只靠 dump。先说结论:默认用 G1,接口对延迟敏感且堆较大时再评估分代 ZGC;堆大小用百分比,不要写死 -Xmx。

GC 怎么选

JDK 21 默认是 G1,大多数业务先用它。分代 ZGC 停顿更短,但更吃内存和 CPU。启动参数里只能留一个收集器,-XX:+UseG1GC 和 -XX:+UseZGC 同时出现时 JVM 会直接启动失败,切换时记得删掉旧参数。

维度 G1(默认) 分代 ZGC
适用堆 2G~16G 最常见,更大也能跑 8G 以上更划算,小堆收益不明显
停顿目标 通常 50~200ms,目标不是硬约束 通常几毫秒内,更稳
吞吐 更好,CPU 开销更低 略差,换来的是延迟平稳
内存额外开销 相对小 更大,容器要多留余量
什么时候用 JAF / 中间件默认,先用这个 慢请求压不下去、堆持续变大时再评估
  • G1 停顿目标:别写成 20ms 这种达不到的值。达不到时回收会更勤、吞吐更差,停顿也不会真到 20ms。堆 4G~8G 的在线服务,从 200ms 起步更现实。

  • IHOP:老年代涨得快、回收跟不上时,把 InitiatingHeapOccupancyPercent 从默认 45 降到 35~40;回收过于空闲时再略调高。

Plain 复制代码
java -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:InitiatingHeapOccupancyPercent=45 \
     -XX:+ParallelRefProcEnabled \
     -XX:+ExitOnOutOfMemoryError \
     -Xlog:gc*,gc+heap=info:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=50M \
     -jar app.jar

分代 ZGC 不必堆很多调优项,给够堆和 CPU 更要紧。容器只给 1 核时回收会拖很久,表现为没有整堆回收、接口却超时。上线后对照三项:慢请求有没有下降、CPU 有没有明显升高、容器内存是不是更贴近上限。切换参数是 -XX:+UseZGC -XX:+ZGenerational。ZGC 自身占用的内存比 G1 多,堆占比从 65% 起步,GC 压力偏高时再调到 70% 左右。

  • GC 日志用 -Xlog,别再用 PrintGCDetails。至少留几天,才能对上某次发布或流量尖峰。

  • System.gc() 在 G1 上会触发整堆回收。生产用 -XX:+ExplicitGCInvokesConcurrent 改成并发回收,不要用 -XX:+DisableExplicitGC 直接关掉:堆外内存打满时,NIO 要靠 System.gc() 回收 DirectByteBuffer,关掉反而更容易 OOM。

  • Netty / NIO 的堆外内存不会出现在堆内存图里,打满时看起来像堆很空却 OOM,要单独设 MaxDirectMemorySize。

容器内存和 CPU

JDK 21 会读容器的内存上限和 CPU 配额,不再按宿主机估算。以前宿主机 64 核 128G、容器只有 2 核 4G,JVM 却按宿主机来算默认堆和 GC 线程数,很容易超出容器配额。换上 JDK 21 镜像后这类问题大多自动消失,要手调的只剩堆百分比和堆外预留。

一块内存 谁在用 容器里怎么留
Java 堆 对象、G1/ZGC 区域 MaxRAMPercentage:G1 取 70~75,ZGC 取 65~70
Metaspace / CodeCache 类、JIT Spring Boot 3 服务常见 256M~512M 量级,别忽略
DirectMemory / Netty NIO、RPC、网关 显式设 -XX:MaxDirectMemorySize,默认可能接近堆大小
线程栈 载体平台线程 + 虚拟线程栈 虚拟线程栈很小,但载体线程仍按平台线程算
GC 自己占用 收集器 ZGC 明显高于 G1,所以 ZGC 的堆占比不要开到 85% 以上
  • 要更快起堆可以加 InitialRAMPercentage,取上限的一半左右,不要等于上限。两者相等时,堆一开始就按上限申请,内存占用很快逼近 limit,节点超卖时这类实例最先被杀。

  • 容器很小(比如 500m)时 JVM 可能只看到 1 核,回收偏慢;需要时写 -XX:ActiveProcessorCount=2,但不要大于容器 CPU 上限。

镜像用公司统一的 JDK 21 基础镜像,别出现编译用 21、运行用 17 的情况。下面是按 4G 容器、G1 给的启动命令,可以直接拷进部署模板。虚拟线程上线初期打开钉住跟踪,确认热路径干净后再关:

Plain 复制代码
java -XX:+UseG1GC \
     -XX:MaxRAMPercentage=70.0 \
     -XX:InitialRAMPercentage=35.0 \
     -XX:MaxDirectMemorySize=512m \
     -XX:+ExplicitGCInvokesConcurrent \
     -XX:+ExitOnOutOfMemoryError \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/heapdump.hprof \
     -Djdk.tracePinnedThreads=short \
     -Xlog:gc*,gc+heap=info:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=50M \
     -jar app.jar

观测:先看聚合,再下钻

以前一次 thread dump 就能看清谁卡住了。虚拟线程可以到十万级,完整 dump 既占磁盘又没法人工读,而且绝大多数线程只是在等 IO。排障顺序要换:

手段 看什么 怎么用
GC 日志 停顿、老年代占用、分配快慢 发布后对比 30 分钟;ZGC 重点看分配是否卡住,而不是停顿时间
JFR GC、锁、虚拟线程被钉住、IO jcmd <pid> JFR.start name=onboarding settings=profile dumponexit=true
钉住跟踪 虚拟线程在 synchronized 里做 IO -Djdk.tracePinnedThreads=short
线程快照 载体线程和少量业务栈 jcmd <pid> Thread.dump_to_file -format=json /tmp/threads.json,别往群里贴几十万行
动态线程池指标 池大小、拒绝、虚拟线程数 指标前缀 hllci_dtp.*
Monitor 跨虚拟线程的 Trace 同一条虚拟线程上换载体,8.x 也不断链;跨任务必须走 Helper。

上线后先做一次对照,光看进程活着不够:

  • 只切 JDK 21、没开虚拟线程时:GC 停顿和容器内存是否比 JDK 8 更稳

  • 打开虚拟线程后:Trace 不断链、钉住次数趋近于零、阻塞 IO 的慢请求不回退

  • JFR 录 1~2 分钟做对比,比反复 dump 更适合看锁和 IO 热点

六、未来展望

升级完成后,下面几项还会继续演进:

方向 现在 接下来
并发模型 虚拟线程按场景打开,跨任务靠 Helper 传递 Trace 和上下文 IO 密集链路默认走虚拟线程,框架在任务提交处统一接住 Trace 和 ThreadLocal
锁与钉住 热路径把 synchronized 换成 ReentrantLock,上线初期用钉住跟踪把关 JDK 24 起 synchronized 不再钉住载体线程(JEP 491),升到下一个 LTS(JDK 25)后,新代码不必再刻意避开 synchronized
GC 默认 G1,大堆服务单独评估分代 ZGC 分代 ZGC 覆盖更多大堆、低延迟服务,G1 仍是多数业务的默认
运行时基线 堆百分比、JFR 和线程池指标由各服务参照本文自行配置 容器感知的堆百分比写进镜像基线,JFR 与线程池指标写进发布门禁

这次踩过的坑、调过的参数,最终都要沉到框架和基础镜像里。下一次运行时升级,业务要做的最好还是只改一行 parent。

相关推荐
isfox1 小时前
MapTask 工作机制:从读数据到写磁盘,一个 Map 任务的完整一生
后端
aibo8603081 小时前
基于 Spring Boot 的茶铺管理系统设计与实现
java·spring boot·后端
掘金酱1 小时前
带着作品来,坐下来聊|掘金 X-lab · Coffee Talk 邀请你来见一面
前端·javascript·后端
唐青枫2 小时前
企业知识库 RAG 实战:混合检索、权限过滤与答案溯源
后端
花椒技术2 小时前
花椒 AI 应用工程化实践:灵泡泡的工作流、版本管理与任务恢复
后端·agent·ai编程
geovindu3 小时前
rust: Mediator Pattern
开发语言·后端·设计模式·rust·中介者模式·行为模式
Doker 多克3 小时前
Spring Integration AMQP 和 Spring AMQP详解
java·后端·spring
韩振方3 小时前
Kubernetes Ingress 用 TLS Secret 给预览域名接上 HTTPS
后端
用户1494484813203 小时前
为什么大厂很少直接用 2PC/TCC?聊聊分布式事务在真实业务里的落地套路
分布式·后端