A项目中遇到一个导出功能,中间有调用到B项目视图,B项目提供的视图在业务高发期极度缓慢,正常运行时间为20-50ms,但是偶尔会慢到七八分钟,最长时会阻塞到两三个小时.故,在多用户同时进行导出时,会占满服务器连接,导致新请求阻塞,项目卡死.跟B系统开发反馈会得知,该视图已经优化多次,继续优化空间不大.同时,数据实时性要求较高,无法添加redis缓存.所以提出解决方案如下:
1.为该请求加上超时时间
2.进行http异步请求
3.限制单个用户同时发起多个请求
4.历史报表进行预下载,保存在服务器
1.首先要添加一个线程池来运行我们的业务代码,从而空出服务器线程以便接受其他请求.
因为我们项目框架是SpringMVC,所以我们要用Spring来创建一个线程池
2.配置spring可以处理异步的http请求
2.1:首先我们要给诶web.xml添加异步支持,这里是servlet3.0之后支持的,所以web-app标签中的version要大于3.0.但是都9012年了,用springmvc的项目都少,更别说太低版本的了.
接着给所有的servlet和filter都加上
true
注意的是,所有的都要加,刚开始只给DispatcherServlet加了,果然有问题.
2.2:Controller中返回特定的结果
2.2.1:Callable接口
返回一个Callable接口,spring会自己帮我们用线程池启动新的线程去执行,这里会使用默认的线程池,有多少请求进来会开多少线程,所以我们需要配置自己需要的线程池来限制.
需要在springmvc的配置里加上
这样可以自定义超时时间和执行业务代码的线程池.
2.2.2 DeferredResult
通过返回这个参数,也可以起到异步的作用,这样可以更灵活的处理业务.
DeferredResult deferredResult = new DeferredResult(800_000L);
deferredResult.onTimeout(() ->
deferredResult.setResult("fail")
);
try {
asyncHttpExecutor.submit(() -> {
try {
ShardedJedis je = shardedJedisPool.getResource();
if (je.exists(userId)) {
je.close();
deferredResult.setResult("处理中,请稍后重试");
return;
} else {
je.setex(userId, 700, "true");
je.close();
}
} catch (Exception e) {
deferredResult.setResult("未知错误");
}finally{
ShardedJedis je = shardedJedisPool.getResource();
System.out.println("delete" + userId);
je.close();
}
});
} catch (RejectedExecutionException e) {
LogHelper.i(this.getClass(), "并发请求过多,请稍后再试");
deferredResult.setResult("并发请求过多,请稍后再试");
}
return deferredResult;
首先,我们新建一个deferredResult,构造函数里可以设置超时时间,到达这个时间后会自动触发timeout方法,但是需要注意的是这里的超时并不会将业务线程终止.
那么,我们应该如何终止业务线程呢:
错误尝试:
首先想到的是用callable接口去处理,async.submit().get()来获取结果
这里有两个问题,
一是主线程中使用get会将主线程卡死,无法起到异步处理的作用,所以我新建里监控线程,在监控线程中get,防止主线程锁死.
二是get后如何终止线程,可以看到的是有shutdown(),shutdonwnnow(),interrupt()等方法,但是不幸的是,这几个方法都无法保证可以真正的停止线程,不像linux里面kill -9,想想也是有道理的,别的线程怎么能处理业务线程的状态呢,万一数据不一致怎么办.(要么处于sleep(),wait()状态可以抛出异常,要么自己的代码里判断isinterrupt 来确定线程是否继续执行)
所以,我在mybatis中设置了超时时间,当sql查询过慢时终止查询.(socket层,statement层,Transaction事务层)这里我设置在statement层.()
在业务处理结束后把返回结果写入deferredResult中就可以了.注意下如果是下载的功能,如果使用了response的write,就不用再写入deferredResult了,这时返回的流已经关闭了,会报错哦.
3,单个用户发起请求的限制
在redis中用用户id为key去做记录,进行业务操作前先判断redis中该key是否存在,这里要注意报错删除key,超时删除key等