
一. 业务场景
目前在企业级系统开发中,前后端分离的开发模式已经非常普遍了。后端服务往往是基于 SpringBoot开发,前端通常使用vue或react实现。此种解决方案的优点多多,此文不再赘述。本文主要讨论在前后端交互中,后端接口如何提高执行效率。
众所周知,在实际开发中经常会出现这样一种情况:在一个前端请求的返回数据结构并不是简单的结构,往往是非常复杂、多维结构数据的聚合结果。在后端的设计中,多维数据往往意味着不同的数据结构(数据表)。后端接口要满足响应数据的开发需求,无法通过多表联查获取数据,这就需要同时做业务1,业务2,业务3,业务n......如果同步执行的话,则本次接口时间取决于业务1~n执行时间之和;如果我们采用异步操作,n项业务同时执行,那么接口耗时取决于执行时间最长的那个业务。这里使用多线程异步执行多项业务,就可以缩短接口调用耗时;从而加快系统的响应时间。
关注Java架构栈,干货天天都不断!
二. 实现思路
本案例中,我们可以通过如下技术实现接口:
url:/goods
Method:GET
输入参数:goodsId(商品ID)
输出参数:ActionResult(统一封装响应类型,返回响应数据中包含商品信息(GoodsInfo)、商品描述(GoodsDesc)、商品评论(GoodsComments))
先通过ThreadPoolTaskExecutor创建线程池,并配置相关参数。在控制层(controller)接口被调用后接收商品ID,先分别调用异步实现的业务逻辑(getGoodsInfo(long goodsId)、getGoodsDesc(long goodsId)、getGoodsComments(long goodsId));等所有业务调用结束后,最后在对返回数据进行封装成统一格式响应给前端。
三. 实现步骤
3.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>3.2 配置线程池
package com.jeffrey.threadpool.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* @ClassName TaskPoolConfig
* @Description TODO
* @Author jeffrey
* @Date 2022/11/11 16:57
* @Version 1.0
**/
@Configuration
@EnableAsync //开启异步服务
public class TaskPoolConfig {
@Bean("taskExecutor") //线程池对象名
public Executor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);//设置核心线程数
executor.setMaxPoolSize(50);//设置最大线程数
executor.setQueueCapacity(200);//线程池组赛队列的容量
executor.setKeepAliveSeconds(60);//线程生存期
executor.setThreadNamePrefix("taskExecutor-"); //线程命名前缀
executor.setWaitForTasksToCompleteOnShutdown(true); //线程池处理线程方案
executor.setAwaitTerminationSeconds(60); //线程池结束等待时间
return executor;
}
}配置线程池的各项参数可以在项目配置文件中定义,再通过@ConfigurationProperties 加载@EnableAsync注解必不可少 ,开启线程池服务。
3.3 各项业务异步实现
在原有的义务方法前添加@Async注解实现异步执行,value值指定异步加入的线程池名称。异步实现的业务方法返回值类型必须是Future接口或其派生类@CompletableFuture,业务层调用dao层接口返回的数据,需要使用AsyncResult的forValue函数进行转换,常见有两种用法:
无返回值的;
可以获取返回值的。
方法返回值不是Futrue类型的,被执行时会立即返回,并且无法获取方法返回值。若需获取异步执行结果,方法返回值必须为Futrue类型,我们可以使用Spring提供的静态方法。
3.4 什么是Futrue?
在并发编程中,我们经常用到非阻塞的模型。在之前的多线程的三种实现中,不管是继承thread类,还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。Future表示一个可能还没有完成的异步任务的结果,针对这个结果,我们可以添加Callback,以便在任务执行成功或失败后作出相应的操作。
package com.jeffrey.threadpool.service;
import com.jeffrey.threadpool.po.Dept;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* @ClassName GoodsService
* @Description TODO 异步获取商品基本信息\描述信息\评论信息
* @Author jeffrey
* @Date 2022/11/11 17:30
* @Version 1.0
**/
@Service
public class GoodsService {
//模拟获取商品基本信息,内部耗时300毫秒
@Async("taskExecutor")
public Future<String> getGoodsInfo(long goodsId) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(300);
return AsyncResult.forValue(String.format("商品%s的基本信息",goodsId));
}
//模拟获取商品描述信息,内部耗时500毫秒
@Async("taskExecutor")
public Future<String> getGoodsDesc(long goodsId) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(500);
return AsyncResult.forValue(String.format("商品%s的描述",goodsId));
}
//模拟获取商品评论信息,内部耗时600毫秒
@Async("taskExecutor")
public Future<List<String>> getGoodsComments(long goodsId) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(600);
List <String> comments = Arrays.asList("评论1","评论2");
return AsyncResult.forValue(comments);
}
@Async("taskExecutor")
public Future<Dept> getDeptById(int id) throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(600);
Dept dept = new Dept(id, "xxxx", "xian");
return AsyncResult.forValue(dept);
}
}3.5 响应数据结构封装
在实际开发中,往往通过封装专门的响应对象来简化响应数据的聚合,再转换成json格式发送到前端。
package com.jeffrey.threadpool.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* @ClassName GoodsVo
* @Description TODO 封装的响应数据结构
* @Author jeffrey
* @Date 2023/2/9 15:25
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsVo implements Serializable {
private String goodsInfo; //商品信息
private String goodsDesc; //商品描述
private List<String> goodsComments; //商品评论信息
}package com.jeffrey.threadpool.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName ActionResult
* @Description TODO 统一的响应格式
* @Author jeffrey
* @Date 2023/2/9 15:23
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ActionResult implements Serializable {
private Integer statusCode; //响应状态码
private String message; //响应短消息
private Object data; //响应数据
}3.6 控制层实现
package com.jeffrey.threadpool.controller;
import com.jeffrey.threadpool.po.Dept;
import com.jeffrey.threadpool.service.GoodsService;
import com.jeffrey.threadpool.vo.ActionResult;
import com.jeffrey.threadpool.vo.GoodsVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@RestController
@Slf4j
public class GoodsController {
@Autowired
GoodsService goodsService;
@GetMapping("/goods")
public ActionResult insertSomething(long goodsId)
throws InterruptedException, ExecutionException {
//异步调用各项业务
//业务1
CompletableFuture<String> goodsInfo = (CompletableFuture<String>) goodsService.getGoodsInfo(goodsId);
//业务2
CompletableFuture<String> goodsDesc = (CompletableFuture<String>) goodsService.getGoodsDesc(goodsId);
// 业务3
CompletableFuture<List<String>> goodsComments = (CompletableFuture<List<String>>) goodsService.getGoodsComments(goodsId);
// 所有任务执行完毕
CompletableFuture.allOf(goodsInfo,goodsDesc).join();
log.info(goodsInfo.get()+" "+goodsDesc.get());
//封装响应数据结构
GoodsVo goodsVo = new GoodsVo(goodsInfo.get(), goodsDesc.get(), goodsComments.get());
ActionResult result = new ActionResult(200, "success", goodsVo);
return result;
}
}四. 注意事项
4.1 在SpringBoot使用线程池注意两点:
添加@EnableAsync注解开启异步,即多线程。在要使用的方法上加上@Async("taskExecutor")注解,括号内是线程池名字。
4.2 @Async失效情况:
异步方法使用static修饰;异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类;异步方法不能与被调用的异步方法在同一个类中;类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象;如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解。
五.总结
本文只是以一个业务场景,来给小伙伴们展现线程池在Springboot中的具体应用,以起到抛砖引玉的效果,提升大伙对线程池技术、高并发场景的学习和探讨。这方面往往也是在就业环节高频出现的面试主题,而有些小伙伴却因为畏难而缺乏这方面准备,导致与心仪的offer失之交臂,实乃惋惜。
扫码咨询了解课程详情,获取课程配套资料!

扫码或者点击文末左下角【阅读原文】快速进入学习!

点击【阅读原文】,免费开始学习Springboot视频课程!
原文作者:Java架构栈;如有侵权,请联系管理员删除。