WebFlux 与响应式编程

共 29 题
📑 题目列表 29 题
#
★★★

1. AIO 在 Spring WebFlux 的应用

请说明 AIO(异步非阻塞 I/O)在 Spring WebFlux 中的应用?

  • AIO(NIO.2 异步 IO)与 Netty 模型
  • WebFlux 基于 Netty 事件循环
  • 非阻塞 IO 与背压

Spring WebFlux 基于 Netty(或 Servlet 3.1+ 的非阻塞容器)实现异步非阻塞 IO。Netty 采用事件循环(EventLoop)模型,基于 NIO 多路复用处理高并发连接,而非传统的线程-per-请求。AIO(NIO.2 异步 IO)是 JDK 的异步 IO 模型,但 Netty 主要基于 NIO 多路复用(Selector)而非 AIO,因为 AIO 在 Linux 上性能不如 NIO 多路复用。WebFlux 利用 Netty 的非阻塞 IO 与 Reactor 背压,实现单线程处理大量连接。核心是"事件循环 + 非阻塞 + 背压",而非 AIO 本身。

WebFlux 依赖 Netty 的 NIO 事件循环模型实现非阻塞,与 JDK AIO 关联不大。理解"事件循环 + 背压"是把握 WebFlux 并发模型的关键。

#
★★★

2. Reactor 的 Context 如何替代 ThreadLocal 在订阅链中逆向传播,为什么它与调用线程无关

请说明 Reactor 的 Context 如何替代 ThreadLocal 在订阅链中逆向传播,以及为什么它与调用线程无关?

  • Reactor Context 是订阅链上下文
  • 逆向传播(订阅时向下游传播)
  • 与线程无关(线程切换上下文仍保留)

Reactor 的 Context(ContextView)是订阅链上的上下文,用于在响应式链中传递数据(如认证信息、请求 ID)。它通过订阅链"逆向传播":Context 在订阅(subscribe)时从下游向上游传播,operator 通过 contextWrite 写值、通过 deferContextual 读取。与 ThreadLocal 的关键区别:Reactor Context 与执行线程无关,因为响应式链可能在任意线程上执行(publishOn/subscribeOn 切换线程),Context 随订阅链传递而非随线程,因此线程切换时上下文不丢失。这使得 Context 适合响应式场景传递线程无关的上下文。

Context 是"订阅链"的自变量,ThreadLocal 是"线程"的自变量。因响应式链可跨线程执行,故用 Context 替代 ThreadLocal 传递上下文,且与线程无关。

#
★★★

3. Reactor 的背压(Backpressure)如何通过 request(n) 协议向上游传递需求,onBackpressureBuffer/Drop/Latest 各自适用什么场景

请说明 Reactor 的背压(Backpressure)如何通过 request(n) 协议向上游传递需求,以及 onBackpressureBuffer/Drop/Latest 各自适用场景?

  • request(n) 背压协议
  • onBackpressureBuffer/Drop/Latest
  • 各自适用场景

Reactor 的背压通过 request(n) 协议实现:下游通过 request(n) 向上游声明可接收的元素数量,上游据此控制生产,实现"下游需求驱动上游生产"。当上游产生过快、下游需求不足时,可用操作符处理:onBackpressureBuffer(缓冲积压,适合可接受延迟与内存占用场景)、onBackpressureDrop(丢弃多余元素,适合数据可丢弃、只需最新一类的场景)、onBackpressureLatest(只保留最新元素,丢弃旧元素,适合实时性要求高、只关心最新状态的场景)。选择依据是"能否丢弃、是否需要最新、内存预算"。

背压是"下游需求驱动"的拉模式。request(n) 控制生产速率,Buffer/Drop/Latest 是超出需求时的三种降级策略,按数据语义选择。

#
★★★

4. WebFlux 基于 Netty EventLoop 的线程模型为何禁止阻塞调用,一次 JDBC 阻塞会怎样拖垮整个事件循环

请说明 WebFlux 基于 Netty EventLoop 的线程模型为何禁止阻塞调用,以及一次 JDBC 阻塞会怎样拖垮整个事件循环?

  • Netty EventLoop 单线程处理事件
  • 阻塞调用占用 EventLoop
  • JDBC 阻塞拖垮事件循环

WebFlux 的 Netty EventLoop 线程模型:每个 EventLoop 线程处理许多连接的事件循环。若在事件循环线程中执行阻塞调用(如同步 JDBC 查询、Thread.sleep),该线程被阻塞,无法处理其他连接的事件,导致所有连接挂起。一次 JDBC 阻塞(如慢查询 100ms)会阻塞整个 EventLoop,使该线程上的所有请求延迟,严重时拖垮整个事件循环。因此 WebFlux 禁止在事件循环线程做阻塞操作,需用 subscribeOn(Schedulers.boundedElastic()) 把阻塞调用放到独立线程池,或用非阻塞数据访问(R2DBC)。

事件循环线程是"共享的"——一个阻塞操作阻塞所有连接。非阻塞设计 + 阻塞调用隔离到线程池是 WebFlux 的铁律。

#
★★★

5. 使用 NIO Selector 实现的简易 HTTP 服务器与 Spring WebFlux 的背压控制相比,吞吐量上限有多大差距

请说明使用 NIO Selector 实现的简易 HTTP 服务器与 Spring WebFlux 的背压控制相比,吞吐量上限有多大差距?

  • 简易 NIO Selector 服务器
  • WebFlux 的背压与事件循环
  • 吞吐量差距

使用 NIO Selector 实现的简易 HTTP 服务器与 WebFlux 都基于非阻塞 IO,但 WebFlux 具备更完善的背压控制、Reactor 组合、Netty 事件循环优化与资源管理。吞吐量差距来自:WebFlux 的背压通过 request(n) 精细化控制各环节生产速率,避免下游积压;Netty 的零拷贝、缓冲池、批量事件处理优化 IO;Reactor 的流水线处理减少空转。简易实现通常缺少背压与缓冲优化,在背压场景下吞吐量差距明显(可达数倍),但在简单场景(无背压需求)差距可能不大。差距主要来自"背压控制 + 事件循环优化"而非非阻塞 IO 本身。

差距不在"非阻塞"而在"背压 + 优化"——WebFlux 精确控制生产速率、优化事件循环,而简易实现在高负载下易积压丢包,吞吐量受限。

#
★★★

6. 虚拟线程(Project Loom)出现后,"是否还需要 Reactive/WebFlux"成为 Java 后端最热争论——两者的适用场景边界如何划分?

请说明虚拟线程(Project Loom)出现后,是否还需要 Reactive/WebFlux,两者的适用场景边界如何划分?

  • 虚拟线程让阻塞变便宜
  • Reactive 的流式/背压优势
  • 适用场景边界

虚拟线程(Project Loom)让阻塞 IO 成本大幅降低,同步编程模型也能获得高并发,因此很多"为了高并发而选 Reactive"的场景可改用"虚拟线程 + 同步代码"。但 Reactive/WebFlux 仍有不可替代场景:流式处理(无限流、背压、窗口聚合)、需要精细控制背压/背压协议、全链路非阻塞(与 R2DBC、非阻塞中间件配合)、系统已有响应式生态。适用场景边界:同步业务、常规 CRUD、事务密集用虚拟线程 + Spring MVC 更简单;流式、背压、高吞吐非阻塞、需要响应式组合与弹性场景用 WebFlux。总之虚拟线程覆盖"并发"需求,Reactive 覆盖"流式/背压/非阻塞"需求,二者互补。

边界是"并发 vs 流式/背压"。虚拟线程解决并发(阻塞变便宜),Reactive 解决流式与背压(本质不可替代)。选型按业务语义而非单纯并发。

#
★★★

7. 已有 WebFlux/Reactor 代码库迁移到虚拟线程的渐进策略,哪些模块先迁、哪些保留 Reactive,双栈共存期间的注意事项如何?

请说明已有 WebFlux/Reactor 代码库迁移到虚拟线程的渐进策略,哪些模块先迁、哪些保留 Reactive,以及双栈共存期间的注意事项?

  • 渐进迁移策略
  • 哪些模块先迁、哪些保留 Reactive
  • 双栈共存注意事项

迁移 WebFlux/Reactor 代码库到虚拟线程应渐进式:先迁移"简单阻塞业务"(如 CRUD、同步事务),这些模块同步化后更简单;保留"流式/背压/实时数据处理"模块为 Reactive(如消息流、数据管道、聚合)。双栈共存期间注意事项:阻塞(MVC + 虚拟线程)与响应式(WebFlux)栈并存时,边界要清晰——阻塞调用不能出现在事件循环线程,响应式调用不能阻塞阻塞线程;数据访问层需统一(阻塞 JDBC 或 R2DBC 二选一,避免混用);线程池与事件循环资源隔离;逐步迁移并做好回归测试。迁移顺序按"业务复杂度 + 阻塞/非阻塞依赖"决定。

渐进迁移按模块粒度:简单阻塞业务先迁,流式/背压模块保留 Reactive。双栈共存的关键是"阻塞与响应式边界清晰、资源隔离、数据层统一"。

#
★★★

8. Spring Boot 3.2+ 中 spring.threads.virtual.enabled=true 开启后,Spring MVC 的线程模型如何变化?与 WebFlux 的功能差异还剩什么?

请说明 Spring Boot 3.2+ 中 spring.threads.virtual.enabled=true 开启后 Spring MVC 线程模型的变化,以及与 WebFlux 的功能差异还剩什么?

  • MVC 用虚拟线程处理请求
  • 与 WebFlux 的功能差异
  • 剩余差异(流式、背压、非阻塞)

开启 spring.threads.virtual.enabled=true 后,Spring MVC 的请求处理改用虚拟线程:每个请求一个虚拟线程,由 JDK 调度到少数平台线程,阻塞 IO 时挂起让出,获得高并发而保持同步编程。与 WebFlux 的功能差异还剩:WebFlux 的全链路非阻塞(无阻塞线程)、流式响应与背压控制、响应式组合(Flux/Mono 的弹性操作)、与 R2DBC/非阻塞生态的天然契合。虚拟线程 + MVC 不提供这些响应式语义,但在"并发 + 同步简单"场景已足够。差异集中在"响应式语义"而非"并发能力"。

虚拟线程让 MVC 获得并发能力,但 WebFlux 的响应式语义(流式、背压、非阻塞组合)仍不可替代。剩余差异是"编程范式"而非"并发"。

#
★★

9. 虚拟线程的 thread-per-request 模型与 Reactive 的 event-loop 模型在吞吐量、延迟、资源占用上的实测对比数据如何解读?

请说明虚拟线程的 thread-per-request 模型与 Reactive 的 event-loop 模型在吞吐量、延迟、资源占用上的实测对比数据如何解读?

  • 两种模型的对比
  • 吞吐量、延迟、资源占用
  • 实测数据解读

实测对比显示:在 IO 密集、阻塞场景下,虚拟线程 thread-per-request 模型与 Reactive event-loop 模型吞吐量接近(虚拟线程通过挂起让出实现高并发),且虚拟线程内存占用更低(无需为每个请求预分配大量线程栈)、代码更简单;在纯计算密集型或极低延迟场景,Reactive 事件循环可能延迟略低(无虚拟线程调度开销),但差距通常不大。资源占用方面,虚拟线程比平台线程池节省大量内存,比 Reactive 的每请求对象开销可控。解读:虚拟线程在"并发 + 简单"上胜出,Reactive 在"流式/背压/极低延迟"上有优势,实测数据应结合业务场景(IO 密集 vs 计算密集)解读。

实测数据表明二者在 IO 密集场景吞吐接近,虚拟线程内存更省、代码更简单,Reactive 在流式与极低延迟场景有优势。解读需结合业务类型。

#
★★

10. JEP 491 修复前 synchronized 与 JNI 阻塞导致的虚拟线程钉住(pinning)如何影响 Reactive 选型决策?如何检测和规避?

请说明 JEP 491 修复前 synchronized 与 JNI 阻塞导致的虚拟线程钉住(pinning)如何影响 Reactive 选型决策,以及如何检测和规避?

  • 虚拟线程钉住(pinning)
  • JEP 491 修复(synchronized 不再钉住)
  • 检测与规避

虚拟线程钉住(pinning)指虚拟线程在 synchronized 块或 JNI 调用中执行阻塞操作时,无法让出载体线程,导致阻塞占位平台线程。JEP 491 修复了 synchronized 在虚拟线程中的钉住问题(使 synchronized 中阻塞不再钉住),但 JNI 阻塞仍可能钉住。影响:若代码大量使用 synchronized 包裹阻塞操作,虚拟线程并发优势受限,可能促使部分团队考虑 Reactive。检测:通过 -Djdk.tracePinnedThreads 跟踪钉住线程;规避:用 ReentrantLock 替代 synchronized(非钉住)、避免在 synchronized 内做阻塞 IO、控制 JNI 使用。对选型影响:钉住风险降低时,虚拟线程更可用,Reactive 选型需求下降。

钉住是虚拟线程的"性能陷阱",JEP 491 修复 synchronized 钉住。检测钉住(tracePinnedThreads)与用 ReentrantLock 规避,是保障虚拟线程并发优势的关键。

#
★★

11. publishOn 与 subscribeOn 对订阅链中线程切换的作用范围与执行位置差异

请说明 publishOn 与 subscribeOn 对订阅链中线程切换的作用范围与执行位置差异?

  • subscribeOn 影响订阅/上游执行
  • publishOn 影响下游执行
  • 作用范围差异

subscribeOn 与 publishOn 都用于指定线程调度器,但作用范围不同:subscribeOn 指定"订阅(上游)执行所在的线程",影响订阅发生时向上游 source 的线程切换,通常作用于链最上游(source 的订阅)。publishOn 指定"下游执行所在的线程",它以下的 operator 在指定线程执行。多个 publishOn 时,每个 publishOn 影响其下游的 operator。subscribeOn 只影响订阅过程(从下游到上游的订阅),publishOn 影响数据流的下游处理。执行位置差异:subscribeOn 决定 source 在哪订阅执行,publishOn 决定后续处理在哪执行。

关键区分:subscribeOn 影响"订阅/上游"(向 source 传播),publishOn 影响"下游处理"。理解作用范围可正确控制响应式链的线程切换。

#
★★

12. 响应式事务为何不能用基于 ThreadLocal 的 @Transactional,TransactionalOperator 与 @Transactional 在 R2DBC 下的编程差异

请说明响应式事务为何不能用基于 ThreadLocal 的 @Transactional,以及 TransactionalOperator 与 @Transactional 在 R2DBC 下的编程差异?

  • ThreadLocal 事务绑定与响应式线程切换
  • TransactionalOperator 编程式响应式事务
  • 与 @Transactional 的差异

@Transactional 基于 ThreadLocal 绑定事务资源,而响应式(R2DBC)链路中线程会切换(publishOn/subscribeOn、事件循环),ThreadLocal 无法跨线程传递事务上下文,因此 @Transactional 在响应式场景失效。R2DBC 使用 TransactionalOperator(编程式),通过 reactive 方式开启/提交/回滚事务,返回的 Mono/Flux 事务包裹执行。编程差异:@Transactional 是声明式(注解)、同步、基于 ThreadLocal;TransactionalOperator 是编程式、响应式、把事务包裹在反应式链中。R2DBC 必须用 TransactionalOperator(或 R2dbcTransactionManager 底层)而非 @Transactional。

响应式异步线程切换使 ThreadLocal 事务失效,故用 TransactionalOperator 以响应式方式管理事务。这是 R2DBC 与 JDBC 事务的根本差异。

#
★★

13. 在响应式链路中需要调用阻塞库时,boundedElastic 调度器的作用与线程上限、队列拒绝行为如何配置

请说明在响应式链路中需要调用阻塞库时 boundedElastic 调度器的作用,以及线程上限、队列拒绝行为如何配置?

  • boundedElastic 隔离阻塞调用
  • 线程上限配置
  • 拒绝行为与队列

在响应式链路中调用阻塞库,必须用 subscribeOn(Schedulers.boundedElastic()) 把阻塞调用放到 boundedElastic 线程池,避免阻塞事件循环线程。boundedElastic 是弹性有界线程池,默认线程上限为处理器核数 × 10(可配置),任务排队时使用队列,队列满时按拒绝策略处理(Schedulers.boundedElastic 的队列有界,溢出会抛异常或排队等待)。可通过配置调整线程上限与队列。作用:隔离阻塞调用,保证事件循环线程不被占用,同时有界限制资源占用。线程上限与队列需按负载预估配置,避免阻塞任务堆积。

boundedElastic 是"阻塞调用隔离池",有界线程上限防资源耗尽。阻塞调用必须用它在独立线程执行,避免污染事件循环。

#
★★

14. 如何用可控分片、慢消费者和连接重置测试 Selector 服务的半包、背压与故障清理语义

请说明如何用可控分片、慢消费者和连接重置测试 Selector 服务的半包、背压与故障清理语义?

  • 半包(半包/粘包)测试
  • 慢消费者与背压
  • 连接重置与故障清理

测试 Selector 服务的半包、背压与故障清理,可通过可控场景:可控分片(把一条消息拆成多个 TCP 分片发送,模拟半包/粘包,验证是否正确重组);慢消费者(消费者不读取或慢读,制造背压,验证缓冲区积压与背压处理);连接重置(主动 RST 关闭连接,验证服务清理通道、缓冲区、SelectionKey 等资源)。这些测试验证服务在"分片重组、背压降级、异常清理"三种边界的正确性。测试设计需控制发送节奏、读取速率与连接异常,观察行为。

半包验证重组、慢消费者验证背压、连接重置验证清理,是网络服务健壮性的三类边界测试。对应半包、慢消费者、故障清理语义。

#
★★

15. Mono 与 Flux 的语义差异是什么,empty/just/defer/fromCallable 等工厂方法在订阅时机上有何不同

请说明 Mono 与 Flux 的语义差异,以及 empty/just/defer/fromCallable 等工厂方法在订阅时机上的不同?

  • Mono 0-1 元素、Flux 0-N 元素
  • 工厂方法订阅时机(立即 vs 延迟)
  • empty/just/defer/fromCallable

Mono 表示 0 或 1 个元素的异步序列,Flux 表示 0 到 N 个元素的异步序列。工厂方法订阅时机不同:just(立即求值,值在创建时已确定);empty(立即返回空序列);defer(延迟创建,每次订阅时才执行 Supplier 生成 publisher);fromCallable(延迟执行 Callable,每次订阅时调用)。关键差异是"立即 vs 延迟":just/empty 订阅时值已定,defer/fromCallable 订阅时才执行生成逻辑。这影响副作用执行时机与每次订阅的独立性。

Mono/Flux 区分元素数量(0-1 vs 0-N),工厂方法区分"立即生成 vs 订阅时生成"。defer/fromCallable 延迟到订阅,适合需要每次订阅重新执行或延迟副作用的场景。

#
★★

16. Reactor 与虚拟线程在 Spring Boot 3.2+ 的实际差异,阻塞 JDBC 调用如何在 reactive 应用中定位?

请说明 Reactor 与虚拟线程在 Spring Boot 3.2+ 的实际差异,以及阻塞 JDBC 调用如何在 reactive 应用中定位?

  • Reactor 响应式 vs 虚拟线程
  • 阻塞 JDBC 在 reactive 应用中的问题
  • 定位阻塞调用

Reactor 与虚拟线程在 Spring Boot 3.2+ 的实际差异:Reactor 是响应式编程范式(非阻塞、背压、事件循环),虚拟线程是并发模型(阻塞变便宜、保持同步),二者解决的问题不同。在 reactive 应用中,若误用阻塞 JDBC(同步 JdbcTemplate),会阻塞事件循环线程,拖垮整个应用。定位:通过线程堆栈(jstack)观察事件循环线程是否被阻塞、监控线程名(reactor-http-nio-*)、用阻塞探测器(BlockHound)检测阻塞调用、检查是否在 subscribeOn 中隔离。定位后应改用 R2DBC 或把阻塞调用放到 boundedElastic。

差异是"响应式范式 vs 并发模型"。reactive 应用中阻塞 JDBC 是致命错误,用线程分析/BlockHound 定位并隔离或改用 R2DBC。

#
★★

17. 虚拟线程与 WebFlux 选型错误的回滚成本如何评估?

请说明虚拟线程与 WebFlux 选型错误的回滚成本如何评估?

  • 选型错误的回滚成本
  • 虚拟线程 vs WebFlux 的切换成本
  • 评估因素

选型错误的回滚成本评估:从虚拟线程(MVC)切到 WebFlux,或反之,成本差异显著。虚拟线程保持同步编程,与既有 MVC 代码兼容,回滚成本低(只改配置/少量代码);WebFlux 是响应式编程范式,代码、数据访问(R2DBC)、测试、操作模型都不同,若选错,回滚到阻塞栈成本高(需重写响应式代码、替换数据访问)。评估因素:代码量、数据访问层(JDBC vs R2DBC)、团队熟悉度、生态依赖、测试成本。风险对冲:若不确定,优先选"虚拟线程 + MVC"(成本低、兼容好),确认有流式/背压强需求再切 WebFlux。总体虚拟线程回滚成本低、WebFlux 回滚成本高。

回滚成本的核心是"编程范式差异"。虚拟线程同步范式兼容性好、回滚成本低,WebFlux 响应式范式切换成本高,选型需谨慎、倾向低风险方案。

#
★★

18. 虚拟线程下数据库连接池(HikariCP)的瓶颈如何显现?为什么"百万虚拟线程"可能只有 50 个数据库连接可用?

请说明虚拟线程下数据库连接池(HikariCP)的瓶颈如何显现,以及为什么"百万虚拟线程"可能只有 50 个数据库连接可用?

  • 虚拟线程并发请求数据库
  • HikariCP 连接池上限
  • 连接池瓶颈

虚拟线程下,应用可创建海量虚拟线程并发请求数据库,但数据库连接池(HikariCP)的 maximumPoolSize 是有限的(默认 10,可配置如 50)。虚拟线程在等待获取连接时挂起,但连接池只有 50 个连接,因此"百万虚拟线程"能同时执行的数据库操作只有 50 个,其余虚拟线程等待连接(blocked 在获取连接)。这凸显连接池是瓶颈:虚拟线程提高并发上限,但数据库连接仍是稀缺资源,连接池大小决定实际数据库并发。理解"虚拟线程并发 ≠ 数据库并发",需合理配置连接池与数据库连接上限。

虚拟线程解决"线程并发",但连接池与数据库连接是稀缺资源。百万虚拟线程受限于连接池大小,实际 DB 并发 = 连接池大小。这是虚拟线程时代的资源瓶颈。

#
★★

19. Structured Concurrency + Scoped Values 与 Reactor Context 在上下文传播上的对比,哪个更适合复杂业务链路?

请说明 Structured Concurrency + Scoped Values 与 Reactor Context 在上下文传播上的对比,哪个更适合复杂业务链路?

  • Structured Concurrency + Scoped Values
  • Reactor Context
  • 上下文传播对比

Scoped Values 是 JDK 的线程上下文值(ScopedValue),与 Structured Concurrency(结构化并发)配合,可在线程(含虚拟线程)间按结构化作用域传播上下文(如请求 ID、认证信息),避免 ThreadLocal 的泄漏与可变问题;Reactor Context 是响应式链的上下文,随订阅链传播。对比:ScopedValues 适合"结构化并发/同步/虚拟线程"模型,明确边界、不可变、无泄漏;Reactor Context 适合"响应式链"模型,随订阅传播。复杂业务链路选择:若用同步/虚拟线程 + 结构化并发,ScopedValues 更合适(作用域清晰、线程安全);若用响应式/WebFlux,Reactor Context 是唯一选择。二者对应不同并发模型,无绝对优劣。

选择取决于并发模型:同步/虚拟线程用 ScopedValues + 结构化并发,响应式用 Reactor Context。ScopedValues 更安全(不可变、无泄漏),Reactor Context 契合响应式。

#
★★

20. Kotlin 协程与 Java 虚拟线程的互操作,suspend 函数如何在虚拟线程上运行,kotlinx-coroutines-reactor 的桥接模式如何?

请说明 Kotlin 协程与 Java 虚拟线程的互操作,suspend 函数如何在虚拟线程上运行,以及 kotlinx-coroutines-reactor 的桥接模式?

  • Kotlin 协程与虚拟线程互操作
  • suspend 函数在虚拟线程运行
  • kotlinx-coroutines-reactor 桥接

Kotlin 协程与 Java 虚拟线程可互操作:suspend 函数运行在协程调度器上,可配置为在虚拟线程(Java 21+)上执行(通过 Dispatchers 或虚拟线程 executor),从而让 suspend 代码获得虚拟线程的并发能力。kotlinx-coroutines-reactor 提供桥接:toFlux/toMono 扩展函数把协程 suspend/Flow 转为 Reactor 类型,reactor 的 Mono/Flux 可通过 awaitFirst/awaitSingle 转为 suspend 挂起操作,实现协程与响应式互转。桥接模式让团队混合使用 Kotlin 协程(同步风格)与 Reactor(响应式),在 WebFlux 中协同。互操作要点是调度器与上下文的转换。

互操作让 suspend 协程与 Reactor 互通,suspend 可在虚拟线程运行,kotlinx-coroutines-reactor 完成协程↔Reactor 桥接,灵活混合两种风格。

#
★★

21. 响应式编程在流式处理(无限流、窗口聚合、背压)场景下仍不可替代的原因是什么?虚拟线程能否模拟这些语义?

请说明响应式编程在流式处理(无限流、窗口聚合、背压)场景下仍不可替代的原因,以及虚拟线程能否模拟这些语义?

  • 无限流、窗口聚合、背压
  • 响应式不可替代的原因
  • 虚拟线程能否模拟

响应式编程在流式处理场景不可替代,因为:无限流(无限数据流需持续处理,无法预先知道数据量)、窗口聚合(按时间/数量窗口聚合数据)、背压(下游需求驱动的拉取控制,反压到上游)。这些语义是"数据驱动的流式处理",虚拟线程是并发模型,无法模拟"无限流 + 窗口 + 背压"的流式语义——虚拟线程只能让阻塞变便宜,不能提供流式组合、背压协议与窗口聚合等原语。响应式(Reactor/Flux)提供这些流式操作符,是其不可替代的根因。虚拟线程可处理"高并发阻塞",但流式处理需响应式。

不可替代的根因是"流式语义"(无限流、窗口、背压)是响应式原语,虚拟线程只是并发模型,无法模拟这些数据流操作。二者维度不同。

#
★★

22. Flux.cache 与 Flux.replay 在热点数据缓存中的应用与内存边界

请说明 Flux.cache 与 Flux.replay 在热点数据缓存中的应用与内存边界?

  • Flux.cache 缓存订阅结果
  • Flux.replay 重放事件
  • 内存边界

Flux.cache 缓存 Flux 的发射结果,使多个订阅者共享同一份数据(避免重复执行 source),适合热点数据缓存(如一次计算多次订阅);Flux.replay 重放已发射的事件给后订阅者,并可缓存历史事件。应用:热点数据缓存(如配置、权限数据)用 cache 缓存结果,避免重复请求。内存边界:cache/replay 会把所有发射元素缓存到内存,若流无限或数据量大,会耗尽内存;需用 cache(long) 限制缓存大小或只缓存最近 N 个(replay 的缓存限制),否则内存溢出。缓存是"内存换计算"的取舍。

cache/replay 通过内存缓存共享结果,但内存边界是风险——无限流/大数据量会 OOM。需用 size 限制缓存量,控制内存占用。

#
★★

23. WebClient 的连接超时、响应超时与重试(retryWhen)配置及与重试幂等的配合

请说明 WebClient 的连接超时、响应超时与重试(retryWhen)配置,以及与重试幂等的配合?

  • 连接超时与响应超时配置
  • retryWhen 重试
  • 重试幂等

WebClient 的超时配置:连接超时通过 ClientHttpConnector(如 Reactor Netty 的 HttpClient)配置,响应超时在 WebClient 调用上加 timeout(Duration)(响应即超时,也可用 exchangeToMono 的 timeout)。重试用 retryWhen(Retry.backoff(...) 或 Retry.max(...)) 实现,控制重试次数、退避与条件。重试幂等配合:重试要求操作幂等(如 GET 查询、幂等写入),否则重试导致重复副作用;可通过幂等键、去重、或只对幂等操作重试。超时与重试结合需考虑总耗时与退避,避免无限重试。

WebClient 超时(连接 + 响应)与 retryWhen 重试保障调用可靠性,重试必须配合幂等(避免重复副作用),并控制退避防雪崩。

#

24. WebClient 的 exchangeToMono 与 retrieve 在响应体释放和错误处理上的差异,未消费响应体会导致什么连接泄漏

请说明 WebClient 的 exchangeToMono 与 retrieve 在响应体释放和错误处理上的差异,以及未消费响应体会导致什么连接泄漏?

  • retrieve 与 exchangeToMono 差异
  • 响应体释放
  • 未消费响应体导致连接泄漏

retrieve 更简洁,自动处理响应体与状态码(4xx/5xx 抛 WebClientResponseException),适合常规情况;exchangeToMono 提供更底层访问(可自定义状态码处理、访问响应头),但需手动释放响应体。关键差异:必须消费响应体(bodyToMono/bodyToFlux)才能释放连接;若用 exchangeToMono 获取响应后未消费 body,连接不会被释放,导致连接泄漏(连接池耗尽)。retrieve 已自动消费释放,exchangeToMono 需保证消费 body 或用 releaseBody 释放。未消费响应体使连接一直占用,最终连接池耗尽。

连接泄漏源于"未消费响应体"。retrieve 自动释放,exchangeToMono 需手动保证消费 body,否则连接泄漏。这是 WebClient 的常见坑。

#

25. 响应式链路中异常如何沿 onError 传播,onErrorResume/onErrorReturn/doFinally 在资源清理与降级中的分工

请说明响应式链路中异常如何沿 onError 传播,以及 onErrorResume/onErrorReturn/doFinally 在资源清理与降级中的分工?

  • onError 传播
  • onErrorResume/onErrorReturn/doFinally
  • 分工

响应式链中异常沿 onError 信号向下游传播,直到被处理(onErrorResume/onErrorReturn)或终止。onErrorResume(用另一个发布者替代错误,如 fallback)、onErrorReturn(返回默认值)、doFinally(无论完成/取消/错误都执行,用于资源清理)。分工:onErrorResume/onErrorReturn 是"降级"(错误恢复),doFinally 是"资源清理"(关闭连接、释放资源)。onErrorResume 可返回 fallback 流,onErrorReturn 返回单一默认值,二者的降级粒度不同;doFinally 保证资源总是释放。合理组合实现"降级 + 清理"。

onError 沿链传播,onErrorResume/onErrorReturn 处理降级,doFinally 保证清理。分工是"错误处理"与"资源释放"分离。

#

26. 虚拟线程池下 ScheduledExecutorService 的兼容性边界在哪?

请说明虚拟线程池下 ScheduledExecutorService 的兼容性边界?

  • ScheduledExecutorService 与虚拟线程
  • 定时任务在虚拟线程的执行
  • 兼容性边界

ScheduledExecutorService 的定时调度(schedule、scheduleAtFixedRate)在虚拟线程下的兼容性:可以用虚拟线程执行定时任务(Executors.newVirtualThreadPerTaskExecutor() 作为执行器),但虚拟线程池的特性是"每任务虚拟线程",与 ScheduledThreadPoolExecutor 的固定线程调度语义不同。兼容性边界:ScheduledExecutorService 的调度语义(固定速率/延迟、线程池大小)在虚拟线程下需注意——虚拟线程池没有固定线程数概念,长驻定时任务可能占满虚拟线程;且虚拟线程的调度由 JDK 管理,ScheduledExecutorService 的固定线程池语义不直接映射。建议:定时任务用 ScheduledExecutorService(固定线程池)或 Spring 的 ThreadPoolTaskScheduler,虚拟线程更适合"每任务"并发而非固定调度。

边界是"固定线程调度 vs 每任务虚拟线程"。ScheduledExecutorService 需要固定线程池语义,虚拟线程池按任务创建,定时调度建议用固定线程池/调度器。

#

27. 在 JDK 21+ 异步 HTTP 客户端(java.net.http.HttpClient)上虚拟线程的弹性优势?

请说明在 JDK 21+ 异步 HTTP 客户端(java.net.http.HttpClient)上虚拟线程的弹性优势?

  • java.net.http.HttpClient 异步 HTTP
  • 虚拟线程弹性优势
  • 阻塞与异步的平衡

java.net.http.HttpClient 提供异步(sendAsync 返回 CompletableFuture)与同步(send)两种方式。在虚拟线程下,用同步 send 即可获得弹性:阻塞等待响应时虚拟线程挂起让出,不占平台线程,无需显式异步回调。虚拟线程的弹性优势:用同步代码写 HTTP 调用,却能以极低资源支撑大量并发请求,比异步回调(CompletableFuture)更简单、可读、无回调地狱。因此虚拟线程下可优先用同步 HttpClient,兼顾简单与高并发。异步 sendAsync 仍适用于需要响应式组合的场景。

虚拟线程让"同步阻塞 HTTP"获得高并发弹性,无需异步回调。这是虚拟线程简化异步 HTTP 编程的核心优势。

#

28. 团队技术选型决策框架,什么团队规模/业务特征/技术债水平下选 Reactive,什么条件下选虚拟线程?

请说明团队技术选型决策框架:什么团队规模/业务特征/技术债水平下选 Reactive,什么条件下选虚拟线程?

  • 选型决策框架
  • 团队规模、业务特征、技术债
  • Reactive vs 虚拟线程

技术选型决策框架:团队规模与技能——若团队熟悉同步编程、规模大、响应式经验不足,选虚拟线程/MVC(学习成本低);若团队已有响应式经验、规模小、愿意投入学习,可选 Reactive。业务特征——流式处理、背压、高吞吐非阻塞、实时数据管道选 Reactive;常规 CRUD、事务、同步业务选虚拟线程。技术债水平——存量代码多为阻塞/同步,选虚拟线程迁移成本低;已有响应式代码库,保留 Reactive。决策原则:优先选"低学习成本 + 低迁移成本 + 匹配业务"的方案,虚拟线程作为默认(简单、兼容好),仅在明确流式/背压/非阻塞强需求时选 Reactive。

决策框架是"团队能力 + 业务需求 + 技术债"三维评估。虚拟线程默认(成本低),Reactive 用于明确流式/背压需求。务实选型。

#

29. StepVerifier 的虚拟时间(withVirtualTime)如何测试定时操作与背压行为

请说明 StepVerifier 的虚拟时间(withVirtualTime)如何测试定时操作与背压行为?

  • StepVerifier 测试响应式
  • withVirtualTime 虚拟时间
  • 测试定时操作与背压

StepVerifier 用于测试响应式流,withVirtualTime 启用虚拟时间(虚拟时钟),不真实等待定时器,快速测试定时操作(如 delayElements、interval、timeout)。通过虚拟时间推进(thenAwait)模拟时间流逝,验证定时行为。测试背压:使用 thenRequest(n) 控制请求元素数,验证 request 信号与背压下的元素发射。StepVerifier 结合虚拟时间测试"定时 + 背压",避免真实延时,快速验证。要点:withVirtualTime 需在订阅前设置,虚拟时间可控推进。

withVirtualTime 用虚拟时钟快进定时操作,thenRequest 控制背压请求,StepVerifier 成为测试定时与背压的利器。