|
问题:A项目中遇到一个导出功能,中间有调用到B项目视图,B项目提供的视图在业务高发期极度缓慢,正常运行时间为20-50ms,但是偶尔会慢到七八分钟,最长时会阻塞到两三个小时.故,在多用户同时进行导出时,会占满服务器连接,导致新请求阻塞,项目卡死.跟B系统开发反馈会得知,该视图已经优化多次,继续优化空间不大.同时,数据实时性要求较高,无法添加redis缓存.所以提出解决方案如下:
1.为该请求加上超时时间
2.进行http异步请求
3.限制单个用户同时发起多个请求
4.历史报表进行预下载,保存在服务器
解决方案:1.首先要添加一个线程池来运行我们的业务代码,从而空出服务器线程以便接受其他请求.
因为我们项目框架是SpringMVC,所以我们要用Spring来创建一个线程池
<!-- 配置异步http请求的线程池 -->
<bean id="asyncHttpExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 最少的线程数 -->
<property name="corePoolSize" value="${asyncHttpExecutor.core_pool_size}"/>
<!-- 最大线程数 -->
<property name="maxPoolSize" value="${asyncHttpExecutor.max_pool_size}"/>
<!-- 缓存队列 -->
<property name="queueCapacity" value="${asyncHttpExecutor.queue_capacity}"/>
<!-- 线程池维护线程所允许的空闲时间,默认为60s -->
<property name="keepAliveSeconds" value="${asyncHttpExecutor.keep_alive_seconds}"/>
<!-- 对拒绝task的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$AbortPolicy"/>
</property>
</bean> |
2.配置spring可以处理异步的http请求2.1:首先我们要给诶web.xml添加异步支持,这里是servlet3.0之后支持的,所以web-app标签中的version要大于3.0.但是都9012年了,用springmvc的项目都少,更别说太低版本的了.
接着给所有的servlet和filter都加上
<async-supported>true</async-supported>
注意的是,所有的都要加,刚开始只给DispatcherServlet加了,果然有问题.
2.2:Controller中返回特定的结果
2.2.1:Callable接口
返回一个Callable接口,spring会自己帮我们用线程池启动新的线程去执行,这里会使用默认的线程池,有多少请求进来会开多少线程,所以我们需要配置自己需要的线程池来限制.
需要在springmvc的配置里加上
<mvc:annotation-driven>
<!-- 可不设置,使用默认的超时时间 -->
<mvc:async-support default-timeout="3000" task-executor="asyncHttpExecutor"/>
</mvc:annotation-driven> |
这样可以自定义超时时间和执行业务代码的线程池.
2.2.2 DeferredResult<String>
通过返回这个参数,也可以起到异步的作用,这样可以更灵活的处理业务.
DeferredResult<String> deferredResult = new DeferredResult<String>(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等
|
网站系统异常
系统异常信息 |
Request URL:
http://www.javathinker.net/WEB-INF/lybbs/jsp/topic.jsp?postID=3518&pages=19
java.lang.NullPointerException
如果你不知道错误发生的原因,请把上面完整的信息提交给本站管理人员。
|
|