Flux和Mimo简介以及SSE实现
(一)Project Reactor 库
注意:请求时,请求头的 Content-Type 任意,但是 response 的 Content-Type 必须为 text/event-stream。
核心类:Flux 和 Mono
Mono:代表 0 或 1 个元素。适用于普通的 REST API(例如查询一次数据库、发送一个普通的 POST 请求),服务器最终只会返回一个完整的 JSON 响应体。
Flux:代表 0 到 N 个元素。适用于流式场景。
常用操作类型
1. 创建操作(如何产生数据流)
Mono.just(value)、Flux.range(start, count)
Mono.fromCallable(() -> ...):将一个同步的阻塞方法包装成异步的 Mono,在订阅时才真正执行。
Flux.interval(Duration):创建一个无限流,按固定时间间隔发射递增的数字,常用于定时任务或心跳检测。
2. 转换操作(如何处理数据)
map
flatMap:异步转换(最常用)。将每个元素转换为一个新的 Mono 或 Flux。
concatMap:与 flatMap 类似,但严格保证顺序。
怎么理解 flatMap 的“扁平化合并”?
在普通的 map 操作中,如果你把一个元素转换成一个流,你会得到一个“流的嵌套”(比如 Flux<Flux
而 flatMap 的核心作用就是“拆盒子”:
映射:把上游的每一个元素,转换成一个内部的流(Publisher)。
扁平化(Flatten):它不会把这些内部流当成独立的对象,而是会立刻订阅这些内部流,并将它们产生的数据“抽出来”,合并到外部的一个主流中。
️ flatMap 和 flatMapMany 有什么区别?
它们的底层逻辑完全一样,最大的区别在于“谁调用它们”以及“返回什么”。
① flatMap(最常用,通用型)
适用对象:Mono 和 Flux 都可以调用。
返回类型:与调用者的类型保持一致。
Mono.flatMap(...) 返回 Mono(1对1异步转换)
Flux.flatMap(...) 返回 Flux(1对N异步转换)
使用场景:处理普通的异步依赖(如网络请求、数据库查询)。
② flatMapMany(专用型)
适用对象:只有 Mono 可以调用。
返回类型:强制返回 Flux(1对N转换)。
使用场景:当你手里只有一个单值(Mono),但你想通过它查出多个结果(Flux)时使用。
-
过滤操作(如何筛选数据)
filter
take(n)
skip(n)
distinct -
组合操作(如何合并多个流)
merge / mergeWith:将多个流并发合并。
concat / concatWith:将多个流串行拼接。
zip / zipWith:将多个流按元素一一对应地组合。 -
错误处理与副作用(如何兜底与调试)
onErrorResume:当流发生异常时,切换到备用的流(Fallback 机制)。
onErrorReturn:当流发生异常时,直接返回一个默认的兜底值。
doOnNext:在不改变数据流的前提下,执行一些“副作用”操作,如打印日志、更新缓存等,是排查问题的利器。
(二)实现 SSE 推送
-
使用 OkHttp 阻塞式请求大模型接口
再用 while 循环使用 Flux.create(sink -> sink.next)。 -
使用 WebClient
点击查看代码
@Bean
public WebClient streamWebClient() {
return WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer your-api-key")
.clientConnector(new ReactorClientHttpConnector(
// 大模型推理耗时较长,需适当延长响应超时时间
HttpClient.create().responseTimeout(Duration.ofMinutes(5))
))
.build();
}
public Flux<String> streamChat(String prompt) {
return webClient.post()
.uri("/v1/chat/completions")
.bodyValue(Map.of("model", "gpt-4", "prompt", prompt, "stream", true))
.accept(MediaType.TEXT_EVENT_STREAM) // 关键:声明接收 SSE 流式响应
.retrieve()
.bodyToFlux(String.class) // 关键:将响应体转为 Flux 流式数据
.doOnNext(chunk -> log.debug("收到数据块: {}", chunk)) // 调试日志
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) // 增强鲁棒性:网络异常时自动重试
.filter(ex -> ex instanceof IOException));
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamToClient(@RequestParam String prompt) {
return streamChat(prompt);
}
- 使用 SseEmitter 和 OkHttp
Call call = client.newCall(request); call.enqueue(new Callback() { ... });
在内部基于 BufferedSource source = responseBody.source(); while (!source.exhausted()) 循环,再用 SseEmitter.send() 发送。
注意:SseEmitter 会强制将 response 的 Content-Type 默认为 text/event-stream。
(三)对于 Flux.create(sink -> sink.subscribe()) 的补充
响应式编程中 Subscribe 的两种触发机制
在响应式编程中,数据流天生具有“惰性”。只有当订阅者触发时,底层的网络请求或数据处理才会真正执行。
-
常规返回:Spring 框架自动订阅
在 Spring WebFlux 中,当我们在 Controller 层直接返回一个 Flux 对象时,框架会在底层自动接管。它检测到响应式发布者后,会主动调用 subscribe() 驱动整个数据流,将数据写入 HTTP 响应体。在此场景下,开发者无需手动干预。 -
自定义桥接:必须手动触发上游
当你使用 Flux.create 时,意味着你创建了一个全新的数据出口(FluxSink),完全接管了数据的流转控制权。
此时,上游的原始流(rawFlux)与你新创建的 Sink 是断开的。Spring 框架只会对最外层的 Flux 进行自动订阅,它并不知道你内部还隐藏着一个 rawFlux。如果不手动调用 rawFlux.subscribe(),上游数据源将永远处于“休眠”状态,导致你的 Sink 收不到任何数据。
核心总结
在 Flux.create 内部手动调用 subscribe(),本质上是为了“拉取”上游数据。你通过订阅上游流,在 onNext 回调中获取数据,经过自定义逻辑处理后,再主动调用 sink.next() 推入新流。
简而言之:常规返回交由框架自动驱动,而自定义桥接逻辑必须由开发者手动触发上游,才能实现数据的无缝衔接。

浙公网安备 33010602011771号