商品详情页系统的Servlet3异步化实践
在京东工作的这一年多时间里,我在整个商品详情页系统(后端数据源)及商品详情页统一服务系统(页面中异步加载的很多服务,如库存服务、图书相关服务、延保服务等)中使用了Servlet3请求异步化模型,总结了Servlet3请求异步化的一些经验和想法跟大家分享和交流。
我将从如下几点阐述Servlet3异步化之后的好处:
1、为什么实现请求异步化需要使用Servlet3
2、请求异步化后得到的好处是什么
3、如何使用Servlet3异步化
4、一些Servlet3异步化压测数据
首先说下一般http请求处理的流程:
1、容器负责接收并解析请求为HttpServletRequest;
2、然后交给Servlet进行业务处理;
3、最后通过HttpServletResponse写出响应。
在Servlet2.x规范中,所有这些处理都是同步进行的,也就是说必须在一个线程中完成从接收请求、业务处理到响应。
1、为什么实现请求异步化需要使用Servlet3
此处已Tomcat6举例子,Tomcat6没有实现Servlet3规范,它在处理请求时是通过如下方式实现的:
org.apache.catalina.connector.CoyoteAdapter#service
- // Recycle the wrapper request and response
- if (!comet) {
- request.recycle();
- response.recycle();
- } else {
- // Clear converters so that the minimum amount of memory
- // is used by this processor
- request.clearEncoders();
- response.clearEncoders();
- }
在请求结束时会同步进行请求的回收,也就是说请求解析、业务处理和响应必须在一个线程内完成,不能跨越线程界限。
这也就说明了必须使用实现了Servlet规范的容器进行处理,如Tomcat 7.x。
2、请求异步化后得到的好处是什么
2.1、更高的并发能力;
2.2、请求解析和业务处理线程池分离;
2.3、根据业务重要性对业务分级,并分级线程池;
2.4、对业务线程池进行监控、运维、降级等处理。
2.1、更高的并发能力
得益于技术的升级,在JDK7配合Tomcat7压测中获得了不错的性能表现。
2.2、请求解析和业务处理线程池分离
在引入Servlet3之前我们的线程模型是如下样子的:

整个请求解析、业务处理、生成响应都是由Tomcat线程池进行处理,而且都是在一个线程中处理;不能分离线程处理;比如接收到请求后交给其他线程处理,这样不能灵活定义业务处理模型。
引入Servlet3之后,我们的线程模型可以改造为如下样子:

此处可以看到请求解析使用Tomcat单线程;而解析完成后会扔到业务队列中,由业务线程池进行处理;这种处理方式可以得到如下好处:
1、根据业务重要性对业务进行分级,然后根据分级定义线程池;
2、可以拿到业务线程池,可以进行很多的操作,比如监控、降级等。
2.3、根据业务重要性对业务分级,并分级线程池
在一个系统的发展期间,我们一般把很多服务放到一个系统中进行处理,比如库存服务、图书相关服务、延保服务等等;这些服务中我们可以根据其重要性对业务分级别和做一些限制:
1、可以把业务分为核心业务级别和非核心业务级别;
2、为不同级别的业务定义不同的线程池,线程池之间是隔离的;
3、根据业务量定义各级别线程池大小。

此时假设非核心业务因为数据库连接池或者网络问题抖动造成响应时间慢,不会对我们核心业务产生影响。
2.4、对业务线程池进行监控、运维、降级等处理
因为业务线程池从Tomcat中分离出来,可以进行线程池的监控,比如查看当前处理的请求有多少,是否到了负载瓶颈,到了瓶颈后可以进行业务报警等处理。

上图是我们一个简陋的监控图,可实时查看到当前处理情况:正在处理的任务有多少,队列中等待的任务有多少;可以根据这些数据进行监控和预警。
另外我们还可以进行一些简单的运维:

对业务线程池进行扩容,或者业务出问题时立即清空线程池防止容器崩溃等问题;而不需要等待容器重启(容器重启需要耗费数十秒甚至数几十毫秒、而且启动后会有预热问题,而造成业务产生抖动)。
如果发现请求处理不过来,负载比较高,最简单的办法就是直接清空线程池,将老请求拒绝掉,而没有雪崩效应。
因为业务队列和业务线程池都是自己的,可以对这些基础组件做很多处理,比如定制业务队列,按照用户级别对用户请求排序,高级别用户得到更高优先级的业务处理。
3、如何使用Servlet3异步化
对于Servlet3的使用,可以参考我之前的博客:
而在我项目里使用就比较简单:
1、接收请求
- @RequestMapping("/book")
- public void getBook(HttpServletRequest request, @RequestParam(value="skuId") final Long skuId,
- @RequestParam(value="cat1") final Integer cat1, @RequestParam(value="cat2") final Integer cat2) throws Exception {
- oneLevelAsyncContext.submitFuture(request, () -> bookService.getBook(skuId, cat1, cat2));
- }
通过一级业务线程池接收请求,并提交业务处理到该线程池;
2、业务线程池封装
- public void submitFuture(final HttpServletRequest req, final Callable<Object> task) {
- final String uri = req.getRequestURI();
- final Map<String, String[]> params = req.getParameterMap();
- final AsyncContext asyncContext = req.startAsync(); //开启异步上下文
- asyncContext.getRequest().setAttribute("uri", uri);
- asyncContext.getRequest().setAttribute("params", params);
- asyncContext.setTimeout(asyncTimeoutInSeconds * 1000);
- if(asyncListener != null) {
- asyncContext.addListener(asyncListener);
- }
- executor.submit(new CanceledCallable(asyncContext) { //提交任务给业务线程池
- @Override
- public Object call() throws Exception {
- Object o = task.call(); //业务处理调用
- if(o == null) {
- callBack(asyncContext, o, uri, params); //业务完成后,响应处理
- }
- if(o instanceof CompletableFuture) {
- CompletableFuture<Object> future = (CompletableFuture<Object>)o;
- future.thenAccept(resultObject -> callBack(asyncContext, resultObject, uri, params))
- .exceptionally(e -> {
- callBack(asyncContext, "", uri, params);
- return null;
- });
- } else if(o instanceof String) {
- callBack(asyncContext, o, uri, params);
- }
- return null;
- }
- });
- }
- private void callBack(AsyncContext asyncContext, Object result, String uri, Map<String, String[]> params) {
- HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse();
- try {
- if(result instanceof String) {
- write(resp, (String)result);
- } else {
- write(resp, JSONUtils.toJSON(result));
- }
- } catch (Throwable e) {
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); //程序内部错误
- try {
- LOG.error("get info error, uri : {}, params : {}", uri, JSONUtils.toJSON(params), e);
- } catch (Exception ex) {
- }
- } finally {
- asyncContext.complete();
- }
- }
线程池的初始化
- @Override
- public void afterPropertiesSet() throws Exception {
- String[] poolSizes = poolSize.split("-");
- //初始线程池大小
- int corePoolSize = Integer.valueOf(poolSizes[0]);
- //最大线程池大小
- int maximumPoolSize = Integer.valueOf(poolSizes[1]);
- queue = new LinkedBlockingDeque<Runnable>(queueCapacity);
- executor = new ThreadPoolExecutor(
- corePoolSize, maximumPoolSize,
- keepAliveTimeInSeconds, TimeUnit.SECONDS,
- queue);
- executor.allowCoreThreadTimeOut(true);
- executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
- @Override
- public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
- if(r instanceof CanceledCallable) {
- CanceledCallable cc = ((CanceledCallable) r);
- AsyncContext asyncContext = cc.asyncContext;
- if(asyncContext != null) {
- try {
- String uri = (String) asyncContext.getRequest().getAttribute("uri");
- Map params = (Map) asyncContext.getRequest().getAttribute("params");
- LOG.error("async request rejected, uri : {}, params : {}", uri, JSONUtils.toJSON(params));
- } catch (Exception e) {}
- try {
- HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse();
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- } finally {
- asyncContext.complete();
- }
- }
- }
- }
- });
- if(asyncListener == null) {
- asyncListener = new AsyncListener() {
- @Override
- public void onComplete(AsyncEvent event) throws IOException {
- }
- @Override
- public void onTimeout(AsyncEvent event) throws IOException {
- AsyncContext asyncContext = event.getAsyncContext();
- try {
- String uri = (String) asyncContext.getRequest().getAttribute("uri");
- Map params = (Map) asyncContext.getRequest().getAttribute("params");
- LOG.error("async request timeout, uri : {}, params : {}", uri, JSONUtils.toJSON(params));
- } catch (Exception e) {}
- try {
- HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse();
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- } finally {
- asyncContext.complete();
- }
- }
- @Override
- public void onError(AsyncEvent event) throws IOException {
- AsyncContext asyncContext = event.getAsyncContext();
- try {
- String uri = (String) asyncContext.getRequest().getAttribute("uri");
- Map params = (Map) asyncContext.getRequest().getAttribute("params");
- LOG.error("async request error, uri : {}, params : {}", uri, JSONUtils.toJSON(params));
- } catch (Exception e) {}
- try {
- HttpServletResponse resp = (HttpServletResponse) asyncContext.getResponse();
- resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- } finally {
- asyncContext.complete();
- }
- }
- @Override
- public void onStartAsync(AsyncEvent event) throws IOException {
- }
- };
- }
- }
3、业务处理
执行bookService.getBook(skuId, cat1, cat2)进行业务处理。
4、返回响应
在之前封装的异步线程池上下文中直接返回。
5、Tomcat server.xml的配置
- <Connector port="1601" asyncTimeout="10000" acceptCount="10240" maxConnections="10240" acceptorThreadCount="1" minSpareThreads="1" maxThreads="1" redirectPort="8443" processorCache="1024" URIEncoding="UTF-8" protocol="org.apache.coyote.http11.Http11NioProtocol" enableLookups="false"/>
我们升级到了jdk1.8.0_51 +tomcat 8.0.26,在使用Http11Nio2Protocol时遇到一些问题,暂时还是使用的Http11Nio1Protocol。此处可以看到Tomcat线程池我们配置了maxThreads=1,即一个线程进行请求解析。
4、一些Servlet3异步化压测数据
压测机器基本环境:32核CPU、32G内存;jdk1.7.0_71 + tomcat 7.0.57,服务响应时间在20ms+,使用最简单的单个URL压测吞吐量:
1、使用同步方式压测
- siege-3.0.7]# ./src/siege -c100 -t60s -b http://***.item.jd.com/981821
- Transactions: 279187 hits
- Availability: 100.00 %
- Elapsed time: 59.33 secs
- Data transferred: 1669.41 MB
- Response time: 0.02 secs
- Transaction rate: 4705.66 trans/sec
- Throughput: 28.14 MB/sec
- Concurrency: 99.91
- Successful transactions: 279187
- Failed transactions: 0
- Longest transaction: 1.04
- Shortest transaction: 0.00
2.1、 使用Servlet3异步化压测 100并发、60秒:
- siege-3.0.7]# ./src/siege -c100 -t60s -b http://***.item.jd.com/981821 .
- Transactions: 337998 hits
- Availability: 100.00 %
- Elapsed time: 59.09 secs
- Data transferred: 2021.07 MB
- Response time: 0.03 secs
- Transaction rate: 5720.05 trans/sec
- Throughput: 34.20 MB/sec
- Concurrency: 149.79
- Successful transactions: 337998
- Failed transactions: 0
- Longest transaction: 1.07
- Shortest transaction: 0.00
2.2、使用Servlet3异步化压测 600并发、60秒:
- siege-3.0.7]# ./src/siege -c600 -t60s -b http://***.item.jd.com/981821
- Transactions: 370985 hits
- Availability: 100.00 %
- Elapsed time: 59.16 secs
- Data transferred: 2218.32 MB
- Response time: 0.10 secs
- Transaction rate: 6270.88 trans/sec
- Throughput: 37.50 MB/sec
- Concurrency: 598.31
- Successful transactions: 370985
- Failed transactions: 0
- Longest transaction: 1.32
- Shortest transaction: 0.00
可以看出异步化之后吞吐量提升了,但是响应时间长了,也就是异步化并不会提升响应时间,但是会增加吞吐量和增加我们需要的灵活性。
通过异步化我们不会获得更快的响应时间,但是我们获得了整体吞吐量和我们需要的灵活性:请求解析和业务处理线程池分离;根据业务重要性对业务分级,并分级线程池;对业务线程池进行监控、运维、降级等处理。
商品详情页系统的Servlet3异步化实践的更多相关文章
- java亿级流量电商详情页系统的大型高并发与高可用缓存架构实战视频教程
亿级流量电商详情页系统的大型高并发与高可用缓存架构实战 完整高清含源码,需要课程的联系QQ:2608609000 1[免费观看]课程介绍以及高并发高可用复杂系统中的缓存架构有哪些东西2[免费观看]基于 ...
- Freemarker商品详情页静态化服务调用处理
--------------------------------------------------------------------------------------------- [版权申明: ...
- 高并发 Nginx+Lua OpenResty系列(10)——商品详情页
本章以京东商品详情页为例,京东商品详情页虽然仅是单个页面,但是其数据聚合源是非常多的,除了一些实时性要求比较高的如价格.库存.服务支持等通过AJAX异步加载加载之外,其他的数据都是在后端做数据聚合然后 ...
- 自己定义ViewGroup实现仿淘宝的商品详情页
近期公司在新版本号上有一个须要. 要在首页加入一个滑动效果, 详细就是仿照X宝的商品详情页, 拉到页面底部时有一个粘滞效果, 例如以下图 X东的商品详情页,假设用户继续向上拉的话就进入商品图文描写叙述 ...
- 如何用Baas快速在腾讯云上开发小程序-系列4:实现客户侧商品列表、商品详情页程序
版权声明:本文由贺嘉 原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/431172001487671163 来源:腾云阁 h ...
- Ecshop商品详情页显示当前会员等级价格
会员登录状态下,在ECSHOP商品详情页的本店售价中显示当前登录会员对应的等级价格,在未登录状态下,则还默认显示原来的本店售价. 解决方法: 这个需要修改ECSHOP程序代码来实现. 打开文件 /in ...
- ecshop 商品详情页显示同类别下的推荐商品
1.打开goods.php文件找到下面代码 $smarty->assign('goods_rank', get_goods_rank($goods_id)); // 商品的销售排名 在上面的代码 ...
- ecshop 优化_将商品详情页goods.php重命名为shangpin.php
有人说,将商品详情页的文件名 goods.php 改一个名字,对百度收录会有帮助,也许吧,这里不讨论是否有帮助,这里只讲解如何重命名. 例如:我们将 goods.php 改为 shangpin.php ...
- Vue通过id跳转到商品详情页
首页列表: 在这里我用a标签进行跳转,在vue里面使用<router-link></router-link> <router-link :to="{path:' ...
随机推荐
- vue前端框架面试问题汇总
1.active-class是哪个组件的属性?嵌套路由怎么定义?答:vue-router模块的router-link组件. 2.怎么定义vue-router的动态路由?怎么获取传过来的动态参数? 答: ...
- Swagger2常用注解及其说明 (转)
Api 用在Controller中,标记一个Controller作为swagger的文档资源 属性名称 说明 value Controller的注解 description 对api资源的描述 hid ...
- 生成统计数据并导出Excel
需求:看如下表格的统计需求 生产调度中心部门需要从IT技术部门得到这些统计数据 步骤: (1)获取所有的子公司列表 (2)遍历所有的子公司,获取每个子公司的库存信息 (3)遍历所有的库存信息,并对库存 ...
- 在文件保存中 os.getcwd() os.listdir() os.makedirs() os.mkdir() xx.join() ... 等函数 的使用介绍
path = 'C:\\Users\\zhangjunming\\Desktop\\PycharmProjects\\my_mgm' 1.xx.join(obj) 以xx为分隔符 对obj中的元素 ...
- tensorflow点滴笔记
1.模型保存 模型保存需要使用函数 tf.train.Saver(), a)创建saver时,可以指定需要存储的tensor,如果没有指定,则全部保存. b) 创建saver时,可以指定保存的模型个数 ...
- Spring Boot 构建电商基础秒杀项目 (七) 自动校验
SpringBoot构建电商基础秒杀项目 学习笔记 修改 UserModel 添加注解 public class UserModel { private Integer id; @NotBlank(m ...
- React 学习(五) ---- 条件和列表渲染
条件渲染 React中的条件渲染和我们平常写的js 代码一样,都是用的if else, 只不过在if else 中它的返回值是jsx, 根据不同的条件渲染不同的UI. 先写两个组件 //登录的用户显示 ...
- Tyche 2191 WYF的递推式
题目描述 WYF手中有这样一条递推式 WYF并不是想让你帮他做出结果,事实上,给定一个n,他能够迅速算出Fn.WYF只是想单纯的考验一下读者们. 输入描述 仅一行,三个整数N,F1,P 输出描述 仅一 ...
- git在Linux下的安装
参考:https://git-scm.com/book/zh/v1/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git Git 的工作需要调用 curl,zlib,op ...
- Keepalived+Haproxy高可用负载均衡群集
介绍 HAProxy提供高可用性.负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费.快速并且可靠的一种解决方案.HAProxy特别适用于那些负载特大的web站点,这些站点通常又需要会 ...