ITPub博客

首页 > 数据库 > 数据库开发技术 > 基于Springboot前后端分离环境,分析线程池的使用

基于Springboot前后端分离环境,分析线程池的使用

原创 数据库开发技术 作者:Finally_m 时间:2023-02-20 08:03:00 0 删除 编辑
点击上方蓝字关注我们

一. 业务场景

目前在企业级系统开发中,前后端分离的开发模式已经非常普遍了。后端服务往往是基于 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函数进行转换,常见有两种用法:

  1. 无返回值的;

  2. 可以获取返回值的。

方法返回值不是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架构栈;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2021-04-13

  • 博文量
    573
  • 访问量
    243823