SpringBoot中异步操作的深度解析与实践指南

在开发中,优化接口响应速度是一个重要的性能考量。异步操作是一种有效的方法,可以在不阻塞主线程的情况下执行耗时的任务,从而提高接口的响应速度。以下是一些关于异步操作适用场景以及如何在SpringBoot中实现异步方法的详细介绍。

异步操作适用场景

异步方法适用于业务逻辑之间可以相互分割且互不影响的情况。例如:

  1. 生成并发送验证码:在用户注册或找回密码时,生成验证码和发送验证码可以异步执行。用户无需等待验证码发送成功即可收到响应,提升用户体验。
  2. 发送邮件或推送通知:在用户完成某些操作后,如下单或更新个人信息,发送确认邮件或推送通知可以异步处理,避免用户等待。
  3. 日志记录:在用户完成操作后,记录日志信息可以异步执行,不影响主业务流程。
  4. 数据缓存更新:在用户查询数据后,更新缓存数据可以异步进行,避免在主线程中执行耗时的数据库操作。

SpringBoot中的异步方法实现

在SpringBoot中,可以通过@Async注解来实现异步方法。以下是实现步骤和示例:

1. 开启异步支持

首先,需要在配置类中使用@EnableAsync注解来开启异步支持。

@Configuration
@EnableAsync
public class AsyncConfig {// 可以在这里配置线程池等参数
}
2. 使用@Async注解

在需要异步执行的方法上使用@Async注解。这个方法将在单独的线程中执行,而不会阻塞主线程。

@Service
public class AsyncService {@Asyncpublic void performLongRunningTask() {System.out.println("Starting long running task on thread: " + Thread.currentThread().getName());try {Thread.sleep(5000); // 模拟耗时操作} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("Completed long running task on thread: " + Thread.currentThread().getName());}
}
3. 调用异步方法

在业务逻辑中调用标记了@Async的方法,这些方法将在后台线程中异步执行。

@RestController
public class AsyncController {@Autowiredprivate AsyncService asyncService;@GetMapping("/async/task")public String executeAsyncTask() {asyncService.performLongRunningTask();return "Task executed asynchronously";}
}
4. 自定义线程池

为了更好地控制异步任务的执行,可以自定义线程池。在配置类中定义一个ThreadPoolTaskExecutor Bean,并在@Async注解中指定线程池名称。

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10);executor.setMaxPoolSize(20);executor.setQueueCapacity(100);executor.setThreadNamePrefix("AsyncThread-");executor.initialize();return executor;}@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {return new SimpleAsyncUncaughtExceptionHandler();}
}

@Async注解中指定线程池名称:

@Async("customExecutor")
public void performLongRunningTask() {// 任务执行逻辑
}

通过这种方式,可以有效地将耗时操作异步化,从而提高接口的响应速度和用户体验。同时,自定义线程池可以更好地控制异步任务的执行,避免系统资源的浪费。

2.使用CompletableFuture

CompletableFuture是Java 8引入的,它提供了一个可编程的异步API,可以用来编写异步代码。例如:

@Service
public class AsyncService {public CompletableFuture<String> asyncTask() {return CompletableFuture.supplyAsync(() -> {// 异步任务执行的逻辑return "异步任务执行完成";});}
}

这种方式可以让你有更多的控制权,比如可以组合多个异步任务,或者在异步任务完成后执行额外的逻辑。

使用TaskExecutor: 通过实现AsyncConfigurer接口,你可以定义自己的TaskExecutor来执行异步任务。例如:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();// 配置线程池参数return executor;}
}

 然后,你可以在服务中注入这个TaskExecutor并使用它来执行异步任务:

@Service
public class AsyncService {@Autowiredprivate TaskExecutor taskExecutor;public void asyncTask() {taskExecutor.execute(() -> {// 异步任务执行的逻辑});}
}

这种方式给你提供了更多的灵活性,比如可以自定义线程池的大小和行为。

3、使用Servlet 3.0的异步支持: 如果你的应用运行在支持Servlet 3.0的容器中,你可以使用AsyncContext来实现异步请求。例如:

@RequestMapping(value = "/email/servletReq", method = GET)
public void servletReq(HttpServletRequest request, HttpServletResponse response) {AsyncContext asyncContext = request.startAsync();// 设置监听器asyncContext.addListener(new AsyncListener() {@Overridepublic void onComplete(AsyncEvent event) throws IOException {// 完成时的逻辑}// 其他事件处理方法...});// 执行异步逻辑
}

 这种方式适用于处理需要长时间运行的任务,比如文件上传或下载,而不需要阻塞服务器资源。

4、使用WebAsyncTask

WebAsyncTask是Spring MVC提供的一个用于处理异步请求的类,它可以返回一个DeferredResult,允许你稍后设置响应。例如:

@RequestMapping(value = "/email/deferredResultReq", method = GET)
@ResponseBody
public DeferredResult<String> deferredResultReq() {DeferredResult<String> result = new DeferredResult<>(60 * 1000L);// 处理业务逻辑result.setResult("处理结果");return result;
}

 这种方式适用于需要在后台执行一些操作,然后返回结果给客户端的场景

但是上述这些方法有一些局限性,就是处理结果仅仅返回单个值。在一些场景下需要异步不断的向客户端响应处理结果的时候就不能使用这些方法了,那么这时候可以看看下面的这些接口

5、 ResponseBodyEmitter

ResponseBodyEmitter 允许你发送一个流式响应,你可以在需要时发送数据片段,而不需要一次性将所有数据发送给客户端。这非常适合需要长时间运行的任务,或者需要动态生成内容并且逐步发送给客户端的场景。

举个例子:比如说在一个大型分布式系统中,系统管理员要实时监控应用的日志信息,以便快速响应潜在的问题。我们将创建一个模拟的日志生成器,它将在后台线程中生成日志条目,并通过ResponseBodyEmitter将这些条目实时发送到客户端。

@RestController
public class LogController {@GetMapping("/logs")public ResponseBodyEmitter streamLogs() {final ResponseBodyEmitter emitter = new ResponseBodyEmitter();// 启动一个新的线程来模拟日志生成new Thread(() -> {try {for (int i = 0; ; i++) {// 模拟日志消息String logEntry = "Log entry " + i + " at " + new Date();// 发送日志消息到客户端emitter.send(logEntry + "\n", MediaType.TEXT_PLAIN);// 模拟日志生成的时间间隔Thread.sleep(1000);}} catch (InterruptedException e) {// 如果线程被中断,则完成发送Thread.currentThread().interrupt();emitter.complete();} catch (IOException e) {// 如果发生IO异常,则以错误结束发送emitter.completeWithError(e);}}).start();return emitter;}
}

在这个例子中,创建了一个新的线程来模拟日志生成。这个线程会无限循环,生成日志条目,并通过ResponseBodyEmitter将它们发送到客户端。

6. SseEmitter

SseEmitter 是一种特殊的ResponseBodyEmitter,它提供了服务器发送事件(Server-Sent Events,SSE)的支持。它允许服务器主动向客户端推送实时数据。这种单向通信机制类似于你观看直播时的视频流,数据源源不断地从服务器流向客户端,而不是等待客户端定期来拉取。

SSE在服务器和客户端之间开了一条单向通道,服务端响应的是text/event-stream类型的数据流信息,在有数据变更的时候从客户端流式的传输到客户端。

想象一下,你正在观看一场足球比赛的直播,每一秒都可能有新的比分或精彩瞬间。作为观众,你希望能够立即看到这些更新,而不是每隔几分钟刷新一次页面来查看最新情况。这就是SseEmitter发挥作用的地方。

实现实时数据推送的步骤

首先,我们需要一个Spring Boot控制器来处理SSE连接,并发送消息给客户端。


@RestController
public class SseEmitterController {private final ExecutorService executor = Executors.newCachedThreadPool();// 用于存储所有的SseEmitter,以便我们可以发送消息给特定的客户端private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();@GetMapping(value = "/subSseEmitter/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public SseEmitter sseEmitter(@PathVariable String userId) {SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);emitters.add(emitter);emitter.onCompletion(() -> emitters.remove(emitter));emitter.onTimeout(() -> emitter.complete());// 模拟异步消息发送executor.execute(() -> {try {for (int i = 0; ; i++) {Thread.sleep(1000); // 模拟消息发送延迟emitter.send(SseEmitter.event().data("Message " + i + " for user " + userId));}} catch (IOException | InterruptedException e) {emitter.completeWithError(e);} finally {emitters.remove(emitter);}});return emitter;}// 用于从其他端点触发消息发送@PostMapping("/sendSseMsg/{userId}")public void sendSseMsg(@PathVariable String userId, @RequestBody String msg) {for (SseEmitter emitter : emitters) {try {emitter.send(SseEmitter.event().data(msg));} catch (IOException e) {// 如果无法发送消息,则移除该emitteremitter.completeWithError(e);emitters.remove(emitter);}}}
}
客户端(HTML + JavaScript)

接下来,我们需要一个简单的HTML页面来连接到SSE端点,并显示从服务器接收到的消息。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>SSE Client Example</title><style>#message {margin-top: 20px;border: 1px solid #ccc;padding: 10px;height: 300px;overflow: auto;}</style>
</head>
<body>
<div id="content" style="text-align: center;"><h1>SSE: Receiving Server-Side Events</h1><div id="message">Waiting for messages...</div>
</div><script>let source = null;let userId = 7777; // 可以替换为动态的用户IDfunction setMessageInnerHTML(message) {const messageDiv = document.getElementById("message");const newParagraph = document.createElement("p");newParagraph.textContent = message;messageDiv.appendChild(newParagraph);}if (window.EventSource) {// Establish a connection//建立连接source = new EventSource(`/subSseEmitter/${userId}`);setMessageInnerHTML("Connected as user=" + userId);//接受到服务端传来的消息source.addEventListener('message', function(event) {setMessageInnerHTML(event.data);});//建立连接后就会触发open事件source.addEventListener('open', function(event) {setMessageInnerHTML("Connection opened.");}, false);source.onerror = function(event) {if (source.readyState === EventSource.CLOSED) {setMessageInnerHTML("Connection was closed.");source.close();}};} else {setMessageInnerHTML("Your browser does not support SSE.");}
</script>
</body>
</html>

当客户端和服务端连接建立后,即使客户端重启也会自动重连

3. StreamingResponseBody

StreamingResponseBody 允许你以流的形式写入响应体,这在处理大文件或实时数据流时非常有用。

举个例子:在一个内容管理系统中,用户可以请求生成一个包含大量数据的年度销售报告。这个报告可能非常大,包含详细的销售数据、图表和分析,如果一次性加载到内存中,可能会导致内存溢出或性能问题。使用StreamingResponseBody可以解决这个问题,因为它允许服务器以流的形式发送数据,从而减少内存使用。

@RestController
public class ReportDownloadController {@GetMapping("/download-large-report")public ResponseEntity<StreamingResponseBody> downloadLargeReport() {return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=annual_sales_report.pdf").contentType(MediaType.APPLICATION_PDF).body(out -> {// 模拟生成PDF文件的过程generateLargeReportPdf(out);});}private void generateLargeReportPdf(OutputStream outputStream) {// 这里使用iText或类似的库来生成PDF// 以下代码仅为示例,实际生成PDF的代码会依赖于具体的库try {// 假设我们有一个方法来生成PDFboolean success = generatePdfDocument(outputStream);if (!success) {// 如果生成失败,可以抛出异常或发送错误消息throw new RuntimeException("Failed to generate the report PDF.");}} catch (IOException e) {throw new UncheckedIOException(e);}}private boolean generatePdfDocument(OutputStream outputStream) throws IOException {// 这里使用iText库来生成PDF// 以下代码仅为示例,实际生成PDF的代码会依赖于具体的库// 模拟生成一个大型PDF文件的过程for (int i = 0; i < 1000; i++) {String content = "Page " + (i + 1) + " - Sales Data as of " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "\n";outputStream.write(content.getBytes());outputStream.flush(); // 调用一次flush就会向客户端写入一次数据try {Thread.sleep(100); // 模拟生成页面的耗时操作} catch (InterruptedException e) {Thread.currentThread().interrupt();}}return true;}
}

在使用 StreamingResponseBody 时,前端下载的文件是合并成一个单一文件的,而不是多个单独的文件。这是因为 StreamingResponseBody 允许服务器以流的形式发送数据,客户端会将这些数据流连续地接收并写入到同一个文件中。

在前面的例子中,服务器端的代码通过循环生成报告的每个部分,并在每次循环中写入输出流 OutputStream。每次调用 out.flush() 时,都会将缓冲区中的数据发送到客户端。客户端浏览器会持续接收这些数据流,并将其作为单一文件处理。

这个过程类似于观看视频流或听音频流,数据是连续不断地发送和接收的,而不是分成多个独立的部分。因此,用户最终得到的是一个完整的PDF文件,而不是多个文件片段。

这里是一个简化的示意图,说明数据是如何被发送和接收的:

服务器端                             客户端
--------                             --------
| 生成数据 | --流式发送--> | 接收数据 | --写入--> | 完整文件 |
| 部分 1   |             | 部分 1   |           | PDF 文件  |
| 部分 2   |             | 部分 2   |           |           |
| ...      |             | ...      |           |           |
| 部分 N   |             | 部分 N   |           |           |
--------                             --------

使用这些工具的注意事项

  • 异常处理:在使用这些工具时,确保妥善处理异常,避免资源泄露。
  • 线程安全:如果你的代码不是线程安全的,确保在多线程环境下正确同步。
  • 性能考虑:虽然这些工具可以提高响应能力,但也要注意不要创建过多的线程,以免影响系统性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/56687.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

光通信——前传基本架构

一、前传基本架构 第三代移动通信技术引入了分布式射频接入网络&#xff08;Distributed Radio Access Network &#xff0c; D-RAN&#xff09;架构。在此架构中的的基带处理单元&#xff08; Baseband Unit &#xff0c; BBU&#xff09; 和 射频拉远单元&#xff08; Remot…

Kafka-初识

一、Kafka是什么&#xff1f; Kafka是一个高度可扩展、弹性、容错和安全的分布式流处理平台&#xff0c;由服务器和客户端组成&#xff0c;通过高性能TCP网络协议进行通信。它可以像消息队列一样生产和消费数据。可以部署在裸机硬件、虚拟机和容器上&#xff0c;也可以部署在本…

[图形学]蒙特卡洛积分方法介绍及其方差计算

一、简介 本文介绍了蒙特卡洛积分算法的基本原理和其误差计算。 二、蒙特卡洛积分介绍 1. 介绍 蒙特卡洛积分算法是一种数值积分算法&#xff0c;用于对复杂函数进行积分。 例如&#xff0c;对于目标积分函数&#xff1a; ∫ a b f ( x ) d x (1) \int_{a}^{b}f(x)\rm{d}x…

【MySQL】提高篇—索引与性能优化:索引的概念与类型(单列索引、复合索引、全文索引)

在关系数据库中&#xff0c;当表中的数据量增大时&#xff0c;查询性能可能会显著下降。为了提高数据检索的效率&#xff0c;数据库系统提供了索引的概念。 索引类似于书籍的目录&#xff0c;可以快速定位到特定的数据行&#xff0c;从而加快查询速度。 索引的使用在实际应用…

springboot034在线商城系统设计与开发-代码(论文+源码)_kaic

毕 业 设 计&#xff08;论 文&#xff09; 题目&#xff1a;ONLY在线商城系统设计与实现 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本ONLY在线商城系统…

Dubbo 序列化方式

Hession 这是dubbo的默认序列化协议&#xff0c;是一种二进制协议&#xff0c;他的特点是序列化的速度比较快&#xff0c;并且序列化的数据体积比较小。Hession适合于大部分场景&#xff0c;因此被选为dubbo的默认序列化协议。 Json Json是一种基于文本的序列化方式&#xf…

python+Mosh网课笔记02

太久没写python代码了&#xff0c;学机器学习重新拾起python&#xff0c;笔记比较简陋。 参考&#xff1a;mosh的python教程 目录 一、控制流 二、函数functions 三、数据结构 list stack queue tuple swapping variables array sets dictionary 解包 一、控制流…

机械臂力控方法概述(一)

目录 1. MoveIt 适用范围 2. 力控制框架与 MoveIt 的区别 3. 力控方法 3.1 直接力控制 (Direct Force Control) 3.2 间接力控制 (Indirect Force Control) 3.2.1 柔顺控制 (Compliant Control) 3.2.2 阻抗控制 (Impedance Control) 3.2.3 导纳控制 (Admittance Control…

Wordpress—一个神奇的个人博客搭建框架

wordpress简介 在当今数字化的时代&#xff0c;拥有一个属于自己的个人博客&#xff0c;不仅可以记录生活点滴、分享专业知识&#xff0c;还能展示个人风采。而在众多的博客搭建框架中&#xff0c;Wordpress 以其强大的功能和灵活性脱颖而出。今天&#xff0c;就让我们一起深入…

【秋招笔试】10.13拼多多(已改编)秋招-三语言题解

🍭 大家好这里是 春秋招笔试突围,一起备战大厂笔试 💻 ACM金牌团队🏅️ | 多次AK大厂笔试 | 大厂实习经历 ✨ 本系列打算持续跟新 春秋招笔试题 👏 感谢大家的订阅➕ 和 喜欢💗 和 手里的小花花🌸 ✨ 笔试合集传送们 -> 🧷春秋招笔试合集 🍒 本专栏已收集…

大数据研究实训室建设方案

一、概述 本方案旨在提出一套全面的大数据研究实训室建设策略&#xff0c;旨在为学生打造一个集理论学习与实践操作于一体的高端教育环境。实训室将专注于培养学生在大数据处理、分析及应用领域的专业技能&#xff0c;通过先进的设施配置、科学的课程体系和实用的实训模式&…

红黑树的底层讲解

一、红黑树的介绍 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是红&#xff08;red&#xff09;或黑&#xff08;black&#xff09;。通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;红…

FLINK SQL语法(1)

DDL Flink SQL DDL&#xff08;Data Definition Language&#xff09;是Flink SQL中用于定义和管理数据结构和数据库对象的语法。以下是对Flink SQL DDL的详细解析&#xff1a; 一、创建数据库&#xff08;CREATE DATABASE&#xff09; 语法&#xff1a;CREATE DATABASE [IF…

linux环境下的程序设计与git操作

目录 前言&#xff1a; 进度条小程序&#xff1a; 先介绍几个背景知识 代码实现 Git操作 总结 其他指令 前言&#xff1a; 本文将重点介绍1. linux下的程序设计&#xff0c;并使用linux下的几个函数接口。实现一个简单的小程序 2.本着开源精神&#xff0c;进行git操作。…

Vue详细入门(语法【三】)

今天滴的学习目标&#xff01;&#xff01;&#xff01; Vue组件是什么&#xff1f;组件的特性和优势Vue3计算属性Vue3监听属性 在前面Vue详细入门&#xff08;语法【一】——【二】&#xff09;当中我们学习了Vue有哪些指令&#xff0c;它的核心语法有哪些&#xff1f;今天我们…

在 javascript 中使用相邻参数作为函数的默认值

更多好文&#xff0c;欢迎关注公众号Geek技术前线 默认参数值在JavaScript中已经存在一段时间了。但其实可以将前面的相邻参数作为默认值本身。 JavaScript自ES2015以来就支持默认参数值&#xff0c;但我们可能不知道的是&#xff0c;我们可以将之前的相邻参数作为默认值 fun…

(AtCoder Beginner Contest 375) 题解(下)

一、题解 第 E 题 3 Team Division 一眼看像背包&#xff0c;观察数据范围&#xff0c;合法的总能力值 ≤ 500 \le 500 ≤500&#xff0c;那么我们可以设计一个背包DP&#xff1a; int dp[110][510][510]; //dp[i][j][k] 表示前 i 个人&#xff0c;分给第一组的能力值是 j&…

微服务架构 --- 使用Seata处理分布式事务

目录 一.Seata 是什么? 1.Seata的核心架构&#xff1a; 2. Seata的分布式事务处理流程&#xff1a; 二.Seata的基本使用&#xff1a; 1.环境准备&#xff1a; 2.引入依赖&#xff1a; 3.加入配置来使用Seata&#xff1a; &#xff08;1&#xff09;首先在nacos上添加一…

免费版视频压缩软件:让视频处理更便捷

现在不少人已经习惯通过视频来记录生活、传播信息和进行娱乐的重要方式。但是由于设备大家现在录制的文件都会比较大&#xff0c;这时候就比较需要一些缩小视频的工具了。今天我们一起来探讨视频压缩软件免费版来为我们带来的生动世界。 1.Foxit视频压缩大师 链接直达&#x…

ESP32移植Openharmony外设篇(1)MQ-2烟雾传感器

外设篇 实验箱介绍 旗舰版实验箱由2部分组成&#xff1a;鸿蒙外设模块&#xff08;支持同时8个工作&#xff09;、鸿蒙平板。 其中&#xff0c;鸿蒙平板默认采用RK3566方案。 OpenHarmony外设模块采用底板传感器拓展板方式&#xff0c;底板默认采用ESP32方案&#xff0c;也…