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

作者:Mars酱

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

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

前言

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

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

RPC 和 响应式 的差异

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

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

建立工程

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

  1. 工程是dubbo框架;
  2. 工程支持 Flowable 背压式响应

于是,我们创建一个dubbo工程,经典的consumer层、facade层、provider层三层结构,像是下面这样:

dubbo provider

这是服务提供者工程,提供者给消费者提供每秒发送一个序列号,序列号是时间的毫秒数,上码:

java 复制代码
@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端写一个单元测试区调用,上码:

java 复制代码
@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对象,比如:

java 复制代码
// 错误:暴露 内部类型
Flux<Event> streamRealTimeEvents();

// 正确:返回普通对象
List<Event> listRealTimeEvents();

修改后的provider是这样,上码:

java 复制代码
@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是这样,上码:

java 复制代码
@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 直接流式处理

java 复制代码
@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,调用 Coze SDK;
  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 的设计原则;
  • 响应式编程应在单机边界内完成,跨网络用数据,不用流。
相关推荐
葫芦和十三5 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp5 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑6 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯7 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan9 小时前
多Agent之间的区别
后端
青石路11 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充11 小时前
1.面向对象设计思想
后端
IT_陈寒11 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro12 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗12 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端