如何提高web应用的吞吐量
这篇博文所列举的优化手段是针对比较传统项目,但是想提高系统的吞吐量现在时髦的技术还是那些前后端未分离, 使用nginx当成静态资源服务器去代理我们的静态资源
是谁限制了Throughput?
当我们对一个传统的项目进行压力测试时,很容器就发现,系统的Throughput被数据库(mysql)限制的死死的,尽管代码看起来确实没毛病,逻辑也没有错误,但是过多的请求都被打向了数据库,数据库自个开启大量的IO操作,这样大的负载甚至会使Linux系统的整体负载骤然飙升,但是反观我们的系统的吞吐量,呵呵...
将目光投向缓存
既然mysql的抗压能力限制了我们的系统,那就将数据缓存起来,尽一切可能减少用户和数据库之间的直接接触的次数,这样我们的系统的吞吐量,同一时间能处理器的请求数量自然会升上去
市面上的缓存技术很多, 比较火爆的是两款缓存数据库 Memcache 和 Redis ,
Redis 和 Memcahe的区别
- Redis不仅仅支持key-value键值对类型的数据,同时还支持list,set,hash等数据结构
 - redis支持数据的备份,即master-slaver模式的集群备份
 - Redis是支持数据持久化的,它可以将内存中的数据保存在磁盘中,支持RDB和AOF两种持久化形式
 
对Redis进行压测
# 挨个测试redis中的命令
# 每个数据包大小是3字节
# 100个并发, 发起10万次请求
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000
[root@139 ~]# redis-benchmark -h 127.0.0.1 -p 9997 -c 100 -n 100000
====== PING_INLINE ======
  100000 requests completed in 1.04 seconds
  100 parallel clients
  3 bytes payload
  keep alive: 1
98.68% <= 1 milliseconds // 百分之98.68的请求在1毫秒内完成了
99.98% <= 2 milliseconds
100.00% <= 2 milliseconds
96525.09 requests per second  // 每秒完成的请求数在9万六左右
-d  指定数据包的大小,看下面redis的性能还是很强大的
-q  简化输出的参数
[root@139 ~]# redis-benchmark -h 127.0.0.1 -p 9997 -q -d 100 -c 100 -n 100000
PING_INLINE: 98619.32 requests per second
PING_BULK: 95877.28 requests per second
SET: 96153.85 requests per second
GET: 95147.48 requests per second
INCR: 95238.10 requests per second
LPUSH: 95328.88 requests per second
RPUSH: 95877.28 requests per second
LPOP: 95328.88 requests per second
RPOP: 97276.27 requests per second
SADD: 96339.12 requests per second
HSET: 98231.83 requests per second
SPOP: 94607.38 requests per second
LPUSH (needed to benchmark LRANGE): 92165.90 requests per second
LRANGE_100 (first 100 elements): 97181.73 requests per second
LRANGE_300 (first 300 elements): 96153.85 requests per second
LRANGE_500 (first 450 elements): 94428.70 requests per second
LRANGE_600 (first 600 elements): 95969.28 requests per second
MSET (10 keys): 98231.83 requests per second
只测试 指定的命令
-t 跟多个命令参数
[root@139 ~]# redis-benchmark -p 9997 -t set,get -q -n 100000 -c 100
SET: 97276.27 requests per second
GET: 98135.42 requests per second
从上面的压力测试中,可以看到,Redis的性能是绝对实力, 相当强悍,和mysql相比不是一个量级的, 所以结论很明显,如果我们在用户和mysql中键加一层redis做缓存,系统的吞吐量自然会上去
于是为了提高系统的抗压能力,我们将压力从mysql逐步转移到redis中
页面缓存技术
在说页面缓存之前,我们先说一下在一个传统的项目中,一个请求的生命周期大概是这样的: 从浏览器发出到服务端, 服务端查询数据库获取结果, 再将结果数据传递给模板引擎将数据渲染进html页面
想提高这个过程的速度,我们可以这样搞, 页面缓存, 顾名思义就是将 html 页面缓存到缓存数据库中
示例如下:
一开始我们会先尝试从缓存中获取出已经渲染好的html源码响应给客户端, 响应的格式通过@ResponseBody和produces中的属性进行控制,告诉浏览器自己会返回给它html文本
优点: 将用户的请求的压力从mysql转移到redis, 这点强度对redis单机来说根本不是事
缺点: 很明显,将请求扩大到页面级别,数据一致性难免会受到影响, 这也是使用页面缓存不得不考虑的一点
特点1 : 严格控制缓存的时间, 一定别忘了添加过期时间...
特点2 : 原来都是让thymeleaf自动完成数据的渲染,现在的话,很明显是我们手动在渲染数据

    @RequestMapping(value = "/to_list",produces = "text/html;charset=UTF-8")
    @ResponseBody
    public String toLogin(Model model, User user, HttpServletResponse response, HttpServletRequest request) {
        // 先从redis缓存中获取数据
        String html = redisService.get(GoodsKey.goodsList, "", String.class);
        if (html != null)
            return html;
        // 查询商品列表
        List<GoodsVo> goodsList = goodsService.getGoodsList();
        model.addAttribute("goodsList", goodsList);
        // 使用Thymeleaf模板引擎手动渲染数据
        WebContext springWebContext = new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());
        String goods_list = thymeleafViewResolver.getTemplateEngine().process("goods_list", springWebContext);
        // 存入redis
        if (goods_list!=null){
            redisService.set(GoodsKey.goodsList,"",goods_list);
        }
        return goods_list;
    }
既然都说到这里了, 就接着说还能怎么玩吧...
你看, 上面通过手动控制模板引擎的api竟然得到的已经渲染好的html源代码了, 什么叫做已经渲染好的? 说白了就是原来我在前端写:th ${user},这样的占位符,现在已经被thymeleaf替换成了 张三 ... (说的够直接吧)
拿到了已经渲染好的源代码,我们就能通过IO操作,将这个文件写到系统的某个目录上去,不知道大家有没有发现,去逛京东淘宝浏览某个商品页面时,就会发现url是类似这样的 www.jjdd.com/aguydg/ahdioa/1235345.html
这个后缀123145.html 大概率说明京东使用静态页的技术, 这太明智了,面对如此巨大数量的商品信息后缀用数字来表示也不错,而且速度还快不是?
怎么实现这种效果呢?
就是上面说的,通过IO将这些源码的数据写到Linux中的某一个目录下面, 文件名就是上面URL中的最后的数字,  通过Nginx做静态资源服务器将这些xxx.html代理起来, 用户再访问的话就走这个静态页, 同样不会接触数据库, 而且nginx还支持零拷贝,并发数5万不是事...
还有,后缀数组最好也别乱写,直接使用商品id会更好,毕竟是先点击商品获取到id,再进入到静态页
对象缓存技术
缓存java中的对象, 比如将用户的信息持久化进redis, 每次用户查询自己的信息先从redis中查询,有的话直接返回,没有的话再去查询数据库, 这样同样实现了在用户和数据库之间多添加出一层缓存,也可以大大的提高系统的吞吐量
一般会怎么玩呢?

用户的请求在查询数据库之前先尝试从redis中获取对象信息, redis中不存在的话就去数据库中查询, 查询完结果后将这个结果换存进redis
// todo 使用redis做缓存,减少和数据库的接触次数
public Label findById(Long labelId) {
    // 先尝试从缓存中查询当前对象
    Label label = (Label) redisTemplate.opsForValue().get("label_id" + labelId);
    if (label==null){
        Optional<Label> byId = labelRepository.findById(labelId);
        if (!byId.isPresent()) {
            // todo 异常
        }
        label = byId.get();
        // 将查出的结果存进缓存中
        redisTemplate.opsForValue().set("label_id"+label.getId(),label);
    }
    return label;
}
当用户update数据 ,先更新数据库,再删除/更新redis中响应的缓存
public void update(Long labelId, Label label) {
    label.setId(labelId);
    Label save = labelRepository.save(label);
    // todo 数据库修改成功后, 将缓存删除
    redisTemplate.delete("label_id"+save.getId());
    }
当用户删除数据,先删除数据库中的数据,再删除redis中的缓存
public void delete(Long labelId) {
    labelRepository.deleteById(labelId);
    // todo 数据库修改成功后, 将缓存删除
    redisTemplate.delete("label_id"+labelId);
}
模仿Vue实现页面静态化
大家都在说页面静态化, 它真的有那么神奇吗? 其实也没有那么神奇, 说白了吧,传统的网页上的数据是通过模板引擎渲染上去的,(比如JSP或者是说thymeleaf这类模板引擎), 做了静态化的网页中数据的渲染通过js完成, 而且这个网页和项目中的 静态资源比如js,css 这类的文件放在一个目录下面, 地位和普通的静态资源相同, 还有个好处就是, 浏览器给的福利, 因为浏览器对静态资源是有缓存的, 如果你善于观察,就会发现有时候重复请求某个网页,网页正常显示,但是状态码是304... 请求依然会到达服务端,但是服务端会告诉浏览器它想访问的页面其实没有变化, 于是浏览器就找到本地的缓存使用
时下国内 最火爆的玩静态页面时下最火爆的技术就是 Angular.js 以及 Vue.js , 也确实好用, 前几个月我写过有关vue的笔记, 感兴趣的同学可以去看看 点击查看我的vue笔记
在本篇博客中恰恰好没用到VUE, 但是实现静态页的思路和vue是大差不差的, 同样是通过js代码实现页面的静态化
- 首先说后端的代码怎么写?
 
前后端分离嘛, 自然是json交互,后端通过@ResponseBody控制返回给前端json对象, 而且, 推荐大家也整一个VO对象,用这个VO对象将各式各样的数据封装在一起,一次性返回给前端, 这样看上去,后端确实是简单,也就是返回一个json对象
- 前端怎么写呢?
 
第一件事就是将html文件从template文件夹下move到static文件夹下面, 让这个html文件和js/css文件称兄道弟
然后是给这个xxx.html该名字, 为啥要改名换目录呢? 因为SpringBoot是约定大于编码的, 在什么目录下面就是什么文件, 此外Thymeleaf部分默认的配置信息如下
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
	private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
	public static final String DEFAULT_PREFIX = "classpath:/templates/";
	public static final String DEFAULT_SUFFIX = ".html";
没办法,这些配置信息默认就认为类路径下的templates中都是xxx.html的文件
第二件事是将html标签中引入的类似thymeleaf这中命名空间都去掉,静态页不需要
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>商品列表</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!-- jquery -->
    <!--<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>-->
    <script type="text/javascript" th:src="js/jquery.min.js"></script>
第三件事写一个ajax,当页面一加载就触发向后台发请求,获取数据, 通过jQuery操作各个节点完成数据的渲染
三步打完收工, 看上也不是很复杂, 但是我们的页面就是已然成为了静态页面, 从此她会被浏览器缓存起来,只要这个页面不发生变动,浏览器就会一直使用自己的缓存,在网络上的数据传输量有多带劲自己脑补,系统RT绝对飙升几个数量级
静态资源的优化手段
说一下市面上常见的静态资源优化技术吧:
- js/css 的压缩与优化,减少流量
 - 多个js/css组合在一起,减少连接数量
 - Tengine 技术
 - webPack , 使用vue开发时,它就会将一整套vue的依赖打包一个js一个html文件,岂不是爽歪歪?
 - CDN加速技术, 很多云服务厂商都有提供,而且价格也不贵,将体型大的静态资源放在CDN上进行加速也是不错的选择
 - 使用Nginx做静态资源代理,而且Nginx就支持零拷贝,还支持将数据文件压缩后在网络上传输 ,自身的并发量也很强大,当静态资源服务器它绝对是不二首选, 相信我,你会爱上它的
 
另外当多个用户并发修改库存时,竟然将库存修改成了负数, 本身使用的数据库引擎是 innodb是存在行级锁的, 我们只要修改一下我们的sql就行 加条件 and stock_number > 0
为了防止同一个用户发送两次请求,偶尔秒杀到多个商品的情况,我们去miaosha_user表中去建立一个唯一的索引,将userId建立唯一索引,不允许相同的userId出现两次,从而避免上述的情况
验证码技术
- 好处
 
让用户去输入验证码的好处有很多, 除了验证用户的身份信息之外, 最明显的好处就是分散用户对系统的压力, 前端如果不添加图片验证码的可能在1s内系统需要承载1万并发, 但是添加了图片验证码, 就能将这1万并发分散到10秒以内,甚至更多
整体的思路:
图片验证码不过是个image, 所以说前端想展示它,肯定需要一个img标签, 一个比较不好想的地方是啥呢? 就是这个图片的路径,src=啥的问题, 我们可以怎么做呢? 可以直接通过这个往src中写入后端的生成imge的Controller的路径, 每次刷新页面,它就会往这个路径中发起请求, Controller中去生成一个image, 通过HttpServletResponse的获取到输出流, 将生成的图片用流的发送会浏览器,再加上他是img标签,这不就ok了?
百度一下如何生成图片验证码一类的技术,确实真的很多,我就不贴代码了, 感兴趣的同学自行百度,代码一片片的
- 如何实现点击图片完成刷新操作呢?
 
因为这种图片是静态资源,如果你不禁用缓存,这个图片就会被缓存下来, 要想每次点击图片都实现更换验证码的话,参考下面的js实现, 添加时间戳
   function refreshImageCode() {
        $("#verifyCodeImg").attr("src","/path/verifyCode?goodsId="+$("#goodsId").val()+"×tamp="+new Date());
    }
接口限流技术
- 什么是接口限流?
 
举个例子: 如说我们想限制在一分钟内单个用户访问 A Controller中的a方法的次数不能超过30次, 这其实就是一种接口限流的需求, 可以有效的防止用户的恶意访问
- 如何实现接口限流呢?
 
其实这件事结合缓存来实现并非是一件难事,比如我就用上面的例子: 不是想对a方法进行限流吗? 我们就在a方法中添加下面的逻辑
伪代码如下:
public void a(User user){
    // 校验user合法性
    // 限流
   Integer count = redis.get(user.getId());
    if(count!=null&&count>15)
      return ; // 到达了指定的阕值,直接返回不允许继续访问
    if(count==null){
        redis.set(user.getId(),30,1); // 表示当前用户访问了1次, 当前key的有效时间为30s
    }else{
	    redis.incr(user.getId());
    }
}
- 如何不让限流的逻辑侵染业务代码呢
 
我们可以使用拦截器技术, 如果我们的重写了拦截器的preHandler()方法,它就会在执行Controller中的方法前进行回调, 再配合自定义注解技术, 后面简直就是为所以为...
示例:
@Component
public class AccessIntercepter extends HandlerInterceptorAdapter {
    // 在方法执行之前进行拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod){
            HandlerMethod hd = (HandlerMethod) handler;
            LimitAccess methodAnnotation = hd.getMethodAnnotation(LimitAccess.class);
            if (methodAnnotation==null)
                return true;
            // 解析注解
            int maxCount = methodAnnotation.maxCount();
            boolean needLogin = methodAnnotation.needLogin();
            int second = methodAnnotation.second();
            // todo
        }
        return true;
    }
}
结语: 最近又到考试周了,今个周六,下周三考试运筹学... 希望自己能平安度过...
我是bloger 赐我白日梦, 欢迎点赞支持
如何提高web应用的吞吐量的更多相关文章
- 为什么 asnyc await 可以提高web程序的吞吐量
		
(转网上一段话) Web程序天生就是多线程的,且web线程都是跑的线程池线程(使用线程池线程是为了避免不断创建.销毁线程所造成的资源成本浪费),而线程池线程可使用线程数量是一定的,尽管可以设置,但它还 ...
 - (转)对《30个提高Web程序执行效率的好经验》的理解
		
阅读了博客园发布的IT文章<30个提高Web程序执行效率的好经验>,这30条准则对我们web开发是非常有用的,不过大家可能对其中的一些准则是知其然而不知其所以然. 下面是我对这些准则的理解 ...
 - Chrome 开发者工具的Timeline和Profiles提高Web应用程序的性能
		
Chrome 开发者工具的Timeline和Profiles提高Web应用程序的性能 二.减少 HTTP 的请求数 当用户浏览页面时,如果我们在用户第一次访问时将一些信息一次性加载到客户端缓存, ...
 - 解读30个提高Web程序执行效率的好经验
		
其实微博是个好东西,关注一些技术博主之后,你不用再逛好多论坛了,因为一些很好的文章微博会告诉你,最近看到酷勤网推荐的一篇文章<30个提高Web程序执行效率的好经验>,文章写得不错,提到一些 ...
 - 提高 web 应用性能之 CSS 性能调优
		
简介 Web 开发中经常会遇到性能的问题,尤其是 Web 2.0 的应用.CSS 代码是控制页面显示样式与效果的最直接“工具”,但是在性能调优时他们通常被 Web 开发工程师所忽略,而事实上不规范的 ...
 - [转]提高 web 应用性能之 CSS 性能调优
		
简介 Web 开发中经常会遇到性能的问题,尤其是 Web 2.0 的应用.CSS 代码是控制页面显示样式与效果的最直接“工具”,但是在性能调优时他们通常被 Web 开发工程师所忽略,而事实上不规范的 ...
 - 一个Web报表项目的性能分析和优化实践(三) :提高Web应用服务器Tomcat的内存配置,并确认配置正确
		
摘要 上一篇,一个Web报表项目的性能分析和优化实践(一):小试牛刀,统一显示SQL语句执行时间 ,讲述了项目优化的整体背景,重点讲述了统一显示了Web项目SQL语句的执行时间. 本篇,将重点介绍提高 ...
 - 如何提高Web应用系统的性能?
		
随着互联网信息技术的发展,人们逐渐开始习惯在网络上交友.购物.学习.娱乐.工作,甚至是找工作.因此市场对网站的响应速度也提出了新的要求,提高Web应用系统的性能成为急需解决的关键问题.本文将会给出一些 ...
 - 关于如何提高Web服务端并发效率的异步编程技术
		
最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...
 
随机推荐
- 编译 lame for iOS
			
网上找了许多编译lame的教程,结果都是编译失败,多次尝试后发现是编译脚本放错路径了,记录下编译的过程,把编译脚本放到源码文件夹中和修改编译脚本中的目录是关键: 一.首先去Lame官网 http:// ...
 - Spring Boot 2.2 正式发布,大幅性能提升 + Java 13 支持
			
之前 Spring Boot 2.2没能按时发布,是由于 Spring Framework 5.2 的发布受阻而推迟.这次随着 Spring Framework 5.2.0 成功发布之后,Spring ...
 - ToShowDoc拯救不想写文档的你
			
ToShowDoc拯救不想写文档的你 写注释已经够折磨开发者了,显然天天curd的我们再去写文档岂不是分分种要被逼疯. 我想每个人都有这种经历 加了一个参数文档忘了更新 参数名更改文档忘了更新 删掉一 ...
 - 算法学习之剑指offer(一)
			
题目一: 题目描述 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数. 思路1:遍历 ...
 - java和JavaScript的注释区别
			
今天在学习JavaScript的注释时候,想到了跟java注释对比一下有什么区别?下面详细的对比了一下. java的注释 java在使用注释的时候分为3种类型的注释. 单行注释:在注释内容前加符号 “ ...
 - JAVA保留小数点位数
			
/** * java 如何保留指定位数的小数 * @author Administrator * */ public class Test04 { public static void main(St ...
 - unittest介绍
			
unittest框架是python中一个标准的库中的一个模块,该模块包括许多的类如 test case类.test suit类.texttest runner类.texttest resuite类.t ...
 - [系列] go-gin-api 路由中间件 - 签名验证(七)
			
目录 概览 MD5 组合 AES 对称加密 RSA 非对称加密 如何调用? 性能测试 PHP 与 Go 加密方法如何互通? 源码地址 go-gin-api 系列文章 概览 首先同步下项目概况: 上篇文 ...
 - vue-property-decorator用法
			
vue-property-decorator这个库完全依赖于vue-class-component,所以在使用这个库之前请先阅读它, 不管啥反正都是装饰器而已 vue-property-decorat ...
 - 数据结构(四十七)归并排序(O(nlogn))
			
一.归并排序的定义 归并排序(Merging Sort)就是利用归并的思想实现的排序方法.它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n ...