1分钟了解响应式编程 | 合适的架构调整

45 阅读5分钟

作者:Mars酱

声明:本文章由Mars酱编写,部分内容来源于网络,如有疑问请联系本人。

转载:欢迎转载,转载前先请联系我!

前言

上篇我们讲到了dubbo官方的例子,根据响应式的基本概念,dubbo 的 StreamObserver 并不提供内置背压支持。

毕竟RPC的设计初衷不是为了响应式而设计的,看下差异吧

RPC 和 响应式 的差异

RPC响应式
是否阻塞⭕️
返回值单值(一次调用,一次返回值)多值(一次调用,流式返回值)
背压不支持。客户端请求,服务端响应并推送,不管客户端是否有能力消费支持。客户端请求,服务端推送,客户端消费可以控制消费速度,再向服务端拿。

既然有差异,那如何完美处理

建立工程

首先,我们确认一下目标:

  1. 工程是dubbo框架;
  2. 工程支持 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 做异步通知

  1. Dubbo 接收请求后,提交任务到 MQ;
  2. 异步消费 MQ,完成业务消费并推送;
  3. 每收到一个 chunk,通过 WebSocket/SSE 推送给前端;
  4. 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 的设计原则;
  • 响应式编程应在单机边界内完成,跨网络用数据,不用流。