发表图文时,上传接口处理时间长,导致用户前端页面一直卡着转圈:因为接口是同步的,也就是用户上传文件的时候需要等文件上传完毕才会返回结果,当较多人同时或者上传较大文件时候就容易卡死。
❀使用多线程异步上传❀: 用户立马就能获取上传响应结果(此时不包括上传成功与否),文件通过流的形式再后台多线程异步上传,这样缺点也就暴露出来了,也就是用户并不知道文件是否上传成功,对于新产生的这个问题的解决办法也很简单:1、轮询查询上传状态 2、在上传完成逻辑中使用websocket或其他方式向用户推送上传状态;3、没有必要关心上传状态等等;
项目是基于SpringBoot的向七牛云上传文件的服务。
package cn.xhubbq.config;
import cn.xhubbq.utils.Threads;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置
*
* @author 甲粒子
**/
@Configuration
public class ThreadPoolConfig
{
// 核心线程池大小
private int corePoolSize = 50;
// 最大可创建的线程数
private int maxPoolSize = 200;
// 队列最大长度
private int queueCapacity = 1000;
// 线程池维护线程所允许的空闲时间
private int keepAliveSeconds = 300;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor()
{
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
/**
* 执行周期性或定时任务
*/
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService()
{
return new ScheduledThreadPoolExecutor(corePoolSize,
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
new ThreadPoolExecutor.CallerRunsPolicy())
{
@Override
protected void afterExecute(Runnable r, Throwable t)
{
super.afterExecute(r, t);
Threads.printException(r, t);
}
};
}
}
package cn.xhubbq.manager;
import cn.xhubbq.utils.SpringUtils;
import cn.xhubbq.utils.Threads;
import java.util.TimerTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 异步任务管理器
*
* @author 甲粒子
*/
public class AsyncManager
{
/**
* 操作延迟10毫秒
*/
private final int OPERATE_DELAY_TIME = 10;
/**
* 异步操作任务调度线程池
*/
private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
/**
* 单例模式
*/
private AsyncManager(){}
private static AsyncManager me = new AsyncManager();
public static AsyncManager me()
{
return me;
}
/**
* 执行任务
*
* @param task 任务
*/
public void execute(TimerTask task)
{
executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
}
/**
* 停止任务线程池
*/
public void shutdown()
{
Threads.shutdownAndAwaitTermination(executor);
}
}
QiniuUtil.qiniu2即是封装的将文件上传到七牛云的工具:
public static Map<String,Object> qiniu2(List<InputStream> files){
Configuration configuration=new Configuration(Region.region2());
UploadManager uploadManager=new UploadManager(configuration);
String key=null;//默认不指定key的情况下,以文件内容的hash值作为文件名
String imgs="";
int count=0;
Map<String,Object> map = new HashMap<>();
byte [] buffer = new byte[1024*1024*2];// 单个文件不超过2MB
try{
Auth auth=Auth.create(AK,SK);
String upToken=auth.uploadToken("xhubbq");
Response response;
for (InputStream file : files) {
while ( file.read(buffer,0,buffer.length) != -1 ) {
response=uploadManager.put(buffer,key,upToken);
DefaultPutRet putRet = JSONObject.parseObject(response.bodyString(), DefaultPutRet.class);
imgs +=(URL_PREFIX+putRet.key+"|");//注意最后多一个"|"
++count;
}
// 关闭 流 防止内存溢出 ==== xxx ===
file.close();
}
map.put("state",1);
map.put("nums",count);
map.put("imgs",imgs);
return map;
}catch (Exception e){
e.printStackTrace();
map.put("state",0);
map.put("nums",count);
map.put("imgs",imgs);
return map;
}
}
※※ 注意其中的关闭流的代码,因为流是做为参数传入的,一定要关闭流,否则会导致内存泄漏。
首先前端传递过来的文件,会存储到临时文件夹中,即类似这样的一个路径。C:\Users\DELL\AppData\Local\Temp\tomcat.8180.459485606774542746\work\Tomcat\localhost\ROOT\upload_4330f731_2ae0_4955_a484_5862fd4530a2_00000009.tmp (系统找不到指定的文件。)
原因分析
异步执行的时候,主线程结束,临时文件就会被清空了,所以会报错。
解决方案
在开启异步之前,需要把MultipartFile(file)转换为流来进行操作即可file.getInputStream()
【参数说明】:其中AK,SK是七牛云的配置,返回的是上传成功的图片地址和数量,多地址用 | 线隔开的。
Post.BBQ代表是表白墙类型,其他的如许愿墙,失物招领等
package cn.xhubbq.manager.factory;
import cn.xhubbq.pojo.post.*;
import cn.xhubbq.service.post.IPostService;
import cn.xhubbq.service.xguser.IUserService;
import cn.xhubbq.utils.QiniuUtil;
import cn.xhubbq.utils.SpringUtils;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
/**
* 异步工厂(产生任务用)
*
* @author
*/
public class AsyncFactory
{
private static IUserService userService = SpringUtils.getBean(IUserService.class);
private static IPostService postService = SpringUtils.getBean(IPostService.class);
private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
/**
* 测试
* @return 任务task
*/
public static TimerTask posts( String content, int type, int nums, List<InputStream> inputStreams)
{
return new TimerTask()
{
@Override
public void run()
{
if (content!=null){
boolean b = userService.checkContentAndImages(null, content);
if(!b){
return;
}
Map<String, Object> qiniu = QiniuUtil.qiniu2(inputStreams);
if( (int)qiniu.get("state") == 0 ){
return;
}
if( (int)qiniu.get("nums") != nums ){
return;
}
String imgs = (String)qiniu.get("imgs");
switch (type){
case Post.TYPE_BBQ:{
Bbq bbq = JSONObject.parseObject(content, Bbq.class);
bbq.setImgPath(imgs);
postService.savePostInMongoDb(bbq);
}
case Post.TYPE_XYQ:{
Xyq xyq = JSONObject.parseObject(content, Xyq.class);
xyq.setImgPath(imgs);
postService.savePostInMongoDb(xyq);
}
case Post.TYPE_SWZL:{
Swzl swzl = JSONObject.parseObject(content, Swzl.class);
swzl.setImgPath(imgs);
postService.savePostInMongoDb(swzl);
}
case Post.TYPE_TZSC:{
Tzsc tzsc = JSONObject.parseObject(content, Tzsc.class);
tzsc.setImgPath(imgs);
postService.savePostInMongoDb(tzsc);
}
}
}
}
};
}
}
package cn.xhubbq.manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
/**
* 确保应用退出时能关闭后台线程
*
* @author 甲粒子
*/
@Component
public class ShutdownManager
{
private static final Logger logger = LoggerFactory.getLogger("sys-user");
@PreDestroy
public void destroy()
{
shutdownAsyncManager();
}
/**
* 停止异步执行任务
*/
private void shutdownAsyncManager()
{
try
{
logger.info("====关闭后台任务任务线程池====");
AsyncManager.me().shutdown();
}
catch (Exception e)
{
logger.error(e.getMessage(), e);
}
}
}
@ResponseBody
@RequestMapping(value = "/post/v2/{content}/{type}/{nums}",produces = "application/json;charset=UTF-8",method = RequestMethod.POST)
public String postsV2(@PathVariable String content, @PathVariable int type, @PathVariable int nums, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
//上传文件
List<MultipartFile> files;
List<InputStream> inputStreams = new ArrayList<>();
MultipartHttpServletRequest req = (MultipartHttpServletRequest) request;
files = req.getFiles("imgs");
for (MultipartFile file : files) {
try {
inputStreams.add(file.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
AsyncManager.me().execute(AsyncFactory.posts(content,type,nums,inputStreams));
map.put("state", 1);
return JSONObject.toJSONString(map);
}