Spring 实现 3 种异步流式接口,干掉接口超时烦恼
大家好,我是小富~
如何处理比较耗时的接口?
这题我熟,直接上异步接口,使用 Callable
、WebAsyncTask
和 DeferredResult
、CompletableFuture
等均可实现。
但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地向客户端响应处理结果,这些方法就不够看了。
Spring 框架提供了多种工具支持异步流式接口,如 ResponseBodyEmitter
、SseEmitter
和 StreamingResponseBody
。这些工具的用法简单,接口中直接返回相应的对象或泛型响应实体 ResponseEntity<xxxx>
,如此这些接口就是异步的,且执行耗时操作亦不会阻塞 Servlet
的请求线程,不影响系统的响应能力。
下面将逐一介绍每个工具的使用及其应用场景。
ResponseBodyEmitter
ResponseBodyEmitter
适应适合于需要动态生成内容并逐步发送给客户端的场景,例如:文件上传进度、实时日志等,可以在任务执行过程中逐步向客户端发送更新。
举个例子,经常用GPT你会发现当你提问后,得到的答案并不是一次性响应呈现的,而是逐步动态显示。这样做的好处是,让你感觉它在认真思考,交互体验比直接返回完整答案更为生动和自然。
使用ResponseBodyEmitter
来实现下这个效果,创建 ResponseBodyEmitter 发送器对象,模拟耗时操作逐步调用 send 方法发送消息。
注意:ResponseBodyEmitter 的超时时间,如果设置为
0
或-1
,则表示连接不会超时;如果不设置,到达默认的超时时间后连接会自动断开。其他两种工具也是同样的用法,后边不在赘述了
@GetMapping("/bodyEmitter")
public ResponseBodyEmitter handle() {
// 创建一个ResponseBodyEmitter,-1代表不超时
ResponseBodyEmitter emitter = new ResponseBodyEmitter(-1L);
// 异步执行耗时操作
CompletableFuture.runAsync(() -> {
try {
// 模拟耗时操作
for (int i = 0; i < 10000; i++) {
System.out.println("bodyEmitter " + i);
// 发送数据
emitter.send("bodyEmitter " + i + " @ " + new Date() + "\n");
Thread.sleep(2000);
}
// 完成
emitter.complete();
} catch (Exception e) {
// 发生异常时结束接口
emitter.completeWithError(e);
}
});
return emitter;
}
实现代码非常简单。通过模拟每2秒响应一次结果,请求接口时可以看到页面数据在动态生成。效果与 GPT 回答基本一致。
SseEmitter
SseEmitter
是 ResponseBodyEmitter
的一个子类,它同样能够实现动态内容生成,不过主要将它用在服务器向客户端推送实时数据,如实时消息推送、状态更新等场景。在我之前的一篇文章 我有 7种 实现web实时消息推送的方案 中详细介绍了 Server-Sent Events (SSE)
技术,感兴趣的可以回顾下。
SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream
类型的数据流信息,在有数据变更时从服务器流式传输到客户端。
整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。
客户端JS实现,通过一次 HTTP 请求建立连接后,等待接收消息。此时,服务端为每个连接创建一个 SseEmitter
对象,通过这个通道向客户端发送消息。
<body>
<div id="content" style="text-align: center;">
<h1>SSE 接收服务端事件消息数据</h1>
<div id="message">等待连接...</div>
</div>
<script>
let source = null;
let userId = 7777
function setMessageInnerHTML(message) {
const messageDiv = document.getElementById("message");
const newParagraph = document.createElement("p");
newParagraph.textContent = message;
messageDiv.appendChild(newParagraph);
}
if (window.EventSource) {
// 建立连接
source = new EventSource('http://127.0.0.1:9033/subSseEmitter/'+userId);
setMessageInnerHTML("连接用户=" + userId);
/**
* 连接一旦建立,就会触发open事件
* 另一种写法:source.onopen = function (event) {}
*/
source.addEventListener('open', function (e) {
setMessageInnerHTML("建立连接。。。");
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/
source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
} else {
setMessageInnerHTML("你的浏览器不支持SSE");
}
</script>
</body>
在服务端,我们将 SseEmitter
发送器对象进行持久化,以便在消息产生时直接取出对应的 SseEmitter 发送器,并调用 send
方法进行推送。
private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();
@GetMapping("/subSseEmitter/{userId}")
public SseEmitter sseEmitter(@PathVariable String userId) {
log.info("sseEmitter: {}", userId);
SseEmitter emitterTmp = new SseEmitter(-1L);
EMITTER_MAP.put(userId, emitterTmp);
CompletableFuture.runAsync(() -> {
try {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.data("sseEmitter" + userId + " @ " + LocalTime.now())
.id(String.valueOf(userId))
.name("sseEmitter");
emitterTmp.send(event);
} catch (Exception ex) {
emitterTmp.completeWithError(ex);
}
});
return emitterTmp;
}
@GetMapping("/sendSseMsg/{userId}")
public void sseEmitter(@PathVariable String userId, String msg) throws IOException {
SseEmitter sseEmitter = EMITTER_MAP.get(userId);
if (sseEmitter == null) {
return;
}
sseEmitter.send(msg);
}
接下来向 userId=7777
的用户发送消息,127.0.0.1:9033/sendSseMsg/7777?msg=欢迎关注-->程序员小富,该消息可以在页面上实时展示。
而且SSE有一点比较好,客户端与服务端一旦建立连接,即便服务端发生重启,也可以做到自动重连。
StreamingResponseBody
StreamingResponseBody
与其他响应处理方式略有不同,主要用于处理大数据量或持续数据流的传输,支持将数据直接写入OutputStream
。
例如,当我们需要下载一个超大文件时,使用 StreamingResponseBody 可以避免将文件数据一次性加载到内存中,而是持续不断的把文件流发送给客户端,从而解决下载大文件时常见的内存溢出问题。
接口实现直接返回 StreamingResponseBody 对象,将数据写入输出流并刷新,调用一次flush
就会向客户端写入一次数据。
@GetMapping("/streamingResponse")
public ResponseEntity<StreamingResponseBody> handleRbe() {
StreamingResponseBody stream = out -> {
String message = "streamingResponse";
for (int i = 0; i < 1000; i++) {
try {
out.write(((message + i) + "\r\n").getBytes());
out.write("\r\n".getBytes());
//调用一次flush就会像前端写入一次数据
out.flush();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(stream);
}
demo这里输出的是简单的文本流,如果是下载文件那么转换成文件流效果是一样的。
总结
这篇介绍三种实现异步流式接口的工具,算是 Spring 知识点的扫盲。使用起来比较简单,没有什么难点,但它们在实际业务中的应用场景还是很多的,通过这些工具,可以有效提高系统的性能和响应能力。
文中 Demo Github 地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot101/通用功能/springboot-streaming
Spring 实现 3 种异步流式接口,干掉接口超时烦恼的更多相关文章
- 第46天学习打卡(四大函数式接口 Stream流式计算 ForkJoin 异步回调 JMM Volatile)
小结与扩展 池的最大的大小如何去设置! 了解:IO密集型,CPU密集型:(调优) //1.CPU密集型 几核就是几个线程 可以保持效率最高 //2.IO密集型判断你的程序中十分耗IO的线程,只要大于 ...
- Serverless Streaming:毫秒级流式大文件处理探秘
摘要:本文将以图片处理的场景作为例子详细描述当前的问题以及华为云FunctionGraph函数工作流在面对该问题时采取的一系列实践. 文章作者|旧浪:华为云Serverless研发专家.平山:华为云中 ...
- Android流式布局实现
查看我的所有开源项目[开源实验室] 欢迎增加我的QQ群:[201055521],本博客client下载[请点击] 摘要 新项目用到了一种全新布局----Android标签流式布局的功能,正好一直说给大 ...
- 聊一聊 redux 异步流之 redux-saga
让我惊讶的是,redux-saga 的作者竟然是一名金融出身的在一家房地产公司工作的员工(让我想到了阮老师...),但是他对写代码有着非常浓厚的热忱,喜欢学习和挑战新的事物,并探索新的想法.恩,牛逼的 ...
- LINQ标准查询运算符的执行方式-延时之流式处理
linq的延时执行是指枚举时才去一个个生成结果元素. 流式处理是linq延时执行的一种,在生成元素前不需要获取所有源元素,只要获取到的源元素足够计算时,便生成结果元素. 流式处理的标准查询运算符返回值 ...
- [JavaScript,Java,C#,C++,Ruby,Perl,PHP,Python][转]流式接口(Fluent interface)
原文:https://en.m.wikipedia.org/wiki/Fluent_interface(英文,完整) 转载:https://zh.wikipedia.org/wiki/流式接口(中文, ...
- Spring框架是一种非侵入式的轻量级框架
摘自<Spring框架技术> Spring框架是一种非侵入式的轻量级框架 1.非侵入式的技术体现 允许在应用系统中自由选择和组装Spring框架的各个功能模块,并且不强制要求应用系统的类必 ...
- 应答流式RPC 请求流式RPC 向流式RPC 流式RPC的三种具体形式
https://mp.weixin.qq.com/s/pWwSfXl71GQZ3KPmAHE_dA 用Python进行gRPC接口测试(二) 大帆船 搜狗测试 2020-02-07 上期回顾:用P ...
- 流式思想概述和两种获取Stream流的方式
流式思想概述 整体来看,流式思想类似于工厂车间的生产流水线 当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个模型步骤方案,然后再按照方法去执行他 这张图中展示 ...
- Spring IOC三种注入方式(接口注入、setter注入、构造器注入)(摘抄)
IOC ,全称 (Inverse Of Control) ,中文意思为:控制反转, Spring 框架的核心基于控制反转原理. 什么是控制反转?控制反转是一种将组件依赖关系的创建和管理置于程序外部的技 ...
随机推荐
- 【转载】 深入理解TensorFlow中的tf.metrics算子
原文地址: https://mp.weixin.qq.com/s/8I5Nvw4t2jT1NR9vIYT5XA ============================================ ...
- 特朗普开始在YouTube上打竞选广告了 —— 美国总统的竞选广告已经开始媒体投放了
相关: 拜登开始在YouTube上打竞选广告了 -- 美国总统的竞选广告已经开始媒体投放了 PS. 又多了一个猴上台,哈哈哈. 特朗普的竞选资金筹集网站:
- (摘抄) 源码分析multiprocessing的Value Array共享内存原理
原文地址: http://xiaorui.cc/archives/3290 ============================================================ 摘 ...
- fatal error: GL/osmesa.h: No such file or directory
安装mujoco报错: fatal error: GL/osmesa.h: No such file or directory 解决方法: sudo apt install libosmesa6-de ...
- 【转载】 Makefile的静态模式%.o : %.c
版权声明:本文为CSDN博主「猪哥-嵌入式」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/u012351051 ...
- python编程中的circular import问题
循环引入,circular import是编程语言中常见的问题,在C语言中我们可以使用宏定义来处理,在c++语言中我们可以使用宏定义和类的预定义等方式来解决,那么在python编程中呢? 其实在pyt ...
- 11-canvas绘制折线图
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ...
- SMU Summer 2024 Contest Round 3
SMU Summer 2024 Contest Round 3 寻找素数对 题意 给你一个偶数,找到两个最接近的素数,其和等于该偶数. 思路 处理出 1e5 以内的素数,然后遍历,更新最接近的答案. ...
- 存储过程test按钮式灰的
PL/SQL Developer中,存储过程无法调试的问题解决办法 在Oracle10中新建了一个用户,然后编写存储过程在PL/SQL Developer中调试,提示 ORA-0131: Insuff ...
- 一款运行于windows上的linux命令神器-Cmder(已经爱不释手)
一.前言 很多工程师都习惯了使用linux下一些命令,再去用Windows的 cmd 简直难以忍受. 要在windows上运行linux命令,目前比较流行的方式由: GunWin32.Cygwin.W ...