作者:Mars酱
声明:本文章由Mars酱编写,部分内容来源于网络,如有疑问请联系本人。
转载:欢迎转载,转载前先请联系我!
前言
上篇我们讲到了dubbo官方的例子,根据响应式的基本概念,dubbo 的 StreamObserver 并不提供内置背压支持。
毕竟RPC的设计初衷不是为了响应式而设计的,看下差异吧
RPC 和 响应式 的差异
| RPC | 响应式 | |
|---|---|---|
| 是否阻塞 | ⭕️ | ❎ |
| 返回值 | 单值(一次调用,一次返回值) | 多值(一次调用,流式返回值) |
| 背压 | 不支持。客户端请求,服务端响应并推送,不管客户端是否有能力消费 | 支持。客户端请求,服务端推送,客户端消费可以控制消费速度,再向服务端拿。 |
既然有差异,那如何完美处理
建立工程
首先,我们确认一下目标:
- 工程是dubbo框架;
- 工程支持 Flowable 背压式响应
于是,我们创建一个dubbo工程,经典的consumer层、facade层、provider层三层结构,像是下面这样:
dubbo provider
这是服务提供者工程,提供者给消费者提供每秒发送一个序列号,序列号是时间的毫秒数,上码:
@DubboService(protocol = "tri") // ← 显式指定端口
public class EventServiceImpl implements EventService {
@Override
public Flux<Event> streamRealTimeEvents() {
return Flux.interval(Duration.ofSeconds(1))
.map(seq -> new Event("id-" + seq, "Hello at " + System.currentTimeMillis()));
}
}
dubbo对外提供 Flux 流,Flux是reactor-core 这个jar包中的类,它的优势是和spring家族的集成比较好,比如:spring webflux。它是天然支持背压的对象。
dubbo consumer
调用的地方跟我们调用本地接口一样,这是RPC的规范,因此我们在consumer端写一个单元测试区调用,上码:
@SpringBootTest(classes = DubboConsumerApplication.class)
public class EventServiceTest {
private static final Logger log = LoggerFactory.getLogger(EventServiceTest.class);
// 👇 关键:通过 url 直连,不走注册中心
@DubboReference(
protocol = "tri",
url = "tri://127.0.0.1:50051" // ← 直连地址
)
private EventService eventService;
@Test
public void testDubbo() {
Flux<Event> eventFlux = eventService.streamRealTimeEvents();
eventFlux
.take(5) // 只取前5个事件
.doOnNext(data -> {
log.info(">> data:{}", data);
})
.blockLast(); // 阻塞直到最后一个元素到达或流完成
}
}
好了,我们运行检验吧!
工程主要使用 spring-boot + dubbo-triple ,先启动provider工程,当出现
这样的日志的时候,表示启动已经成功了。那么,我们开始流式调用吧,运行consumer中的那个单元测试,结果...
出错了
运行之后,报错!错误提示说是因为 reactor.core.publisher.FluxMap 对象没有实现 Serializable 接口。
为什么会这样?因为RPC的传输协议中对于序列化的检查是有要求的。
就算我们通过配置yaml文件的序列化检查关闭掉、把包加入白名单,都是无济于事无法解决这个问题。
那我又想有背压,又想使用dubbo,怎么办?
完美的解决方案有吗?
有!需要修改结构!
其实,这是一个非常典型的 “内部响应式 + 外部 RPC 同步” 架构问题。你的目标需要修改为:
- provider 层:只传输简单 JavaBean,不暴露 Flux/Flowable;
- consumer 层:接收到 provider层的JavaBean之后,再封装为Flux/Flowable 流;
- 如果还有对外层,比如web层的controller:对外提供 Flux/Flowable 接口(例如用于 SSE、流式响应等);
总之,不应该在RPC的传输层去传输Flux/Flowable对象,RPC的接口只返回正常的POJO对象,比如:
// 错误:暴露 内部类型
Flux<Event> streamRealTimeEvents();
// 正确:返回普通对象
List<Event> listRealTimeEvents();
修改后的provider是这样,上码:
@Override
public List<Event> listRealTimeEvents() {
List<Event> eventList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
eventList.add(new Event("id-" + i, "Hello at " + System.currentTimeMillis()));
try {
Thread.sleep(1000); // 每秒添加一个
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return eventList;
}
修改后的 consumer是这样,上码:
@Test
public void testDubbo2() {
List<Event> events = eventService.listRealTimeEvents();
// 将 List 转换成 Flux 流
Flux<Event> eventFlux = Flux.fromIterable(events);
}
进阶理解
如果你的工程像我这样,有时间上的紧密关联,那么这么做是有问题的!
因为provider层每秒产生一组序列号,返回给consumer层之后,有时间消耗,但是如果你的场景和时间无关的,那么上面这种方式就满足你的场景了,如果场景和时间有关,那么根据你的场景最合适的方式可能就是在consumer层去做Flux/Flowable的流处理,provider则负责处理普通crud逻辑。
因为 Dubbo 是 “请求-响应”模型,必须等 listRealTimeEvents() 完全执行完才能返回 List,中间过程无法实时推送给 consumer,理论上不算是真正的“实时流式传输” 。
真正的流式方案
如果你必须实时推送每个 chunk(如大模型逐字输出),则 不能依赖 Dubbo 作为中间层,以下有几个建议:
建议1:绕过 Dubbo,consumer 直接流式处理
@Test
public void testDubbo2() {
Flux.interval(Duration.ofSeconds(1))
.map(seq -> new Event("id-" + seq, "Hello at " + System.currentTimeMillis()));
}
对,你没看错,本来provider的处理逻辑直接搬到consumer层,这样做
优点:真正实时;
缺点:失去 Dubbo 的服务治理能力(负载均衡、熔断等)。
建议2:使用消息队列 or WebSocket 做异步通知
- Dubbo 接收请求后,提交任务到 MQ;
- 异步消费 MQ,完成业务消费并推送;
- 每收到一个 chunk,通过 WebSocket/SSE 推送给前端;
- Dubbo 接口只返回任务 ID。
建议3:升级到支持响应式 RPC 的框架
- 如 gRPC + Reactor(支持 server streaming);
- 或 Spring Cloud Gateway + WebFlux 直连后端服务。
总结:如何选择?
| 需求 | 推荐方案 |
|---|---|
| 只需最终结果 | Dubbo 返回 Event 普通JavaBean,consumer 直接返回 |
| 需要所有事件(可接受延迟) | Dubbo 返回 List,consumer 转Flowable.fromIterable() |
| 必须实时流式输出 | consumer 直接输出(绕过 Dubbo)或改用消息推送 |
最后提醒
- 不要试图让 Flux/Flowable 穿透 Dubbo —— 这违背了 RPC 的设计原则;
- 响应式编程应在单机边界内完成,跨网络用数据,不用流。