如何提高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进行压测#
Copy
# 挨个测试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自动完成数据的渲染,现在的话,很明显是我们手动在渲染数据
举个例子
Copy
@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 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
Copy
// todo 使用redis做缓存,减少和数据库的接触次数
public Label findById(Long labelId) {
// 先尝试从缓存中查询当前对象
Label label = (Label) redisTemplate.opsForValue().get("label_id" + labelId);
if (label==null){
Optional