这篇博文所列举的优化手段是针对比较传统项目,可是想提升系统的吞吐量如今时髦的技术仍是那些先后端未分离, 使用nginx当成静态资源服务器去代理咱们的静态资源javascript
当咱们对一个传统的项目进行压力测试时,很容器就发现,系统的Throughput被数据库(mysql)限制的死死的,尽管代码看起来确实没毛病,逻辑也没有错误,可是过多的请求都被打向了数据库,数据库自个开启大量的IO操做,这样大的负载甚至会使Linux系统的总体负载骤然飙升,可是反观咱们的系统的吞吐量,呵呵...css
既然mysql的抗压能力限制了咱们的系统,那就将数据缓存起来,尽一切可能减小用户和数据库之间的直接接触的次数,这样咱们的系统的吞吐量,同一时间能处理器的请求数量天然会升上去html
市面上的缓存技术不少, 比较火爆的是两款缓存数据库 Memcache 和 Redis ,前端
Redis 和 Memcahe的区别vue
# 挨个测试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作缓存,系统的吞吐量天然会上去java
因而为了提升系统的抗压能力,咱们将压力从mysql逐步转移到redis中mysql
在说页面缓存以前,咱们先说一下在一个传统的项目中,一个请求的生命周期大概是这样的: 从浏览器发出到服务端, 服务端查询数据库获取结果, 再将结果数据传递给模板引擎将数据渲染进html页面jquery
想提升这个过程的速度,咱们能够这样搞, 页面缓存, 顾名思义就是将 html 页面缓存到缓存数据库中nginx
示例以下:web
一开始咱们会先尝试从缓存中获取出已经渲染好的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); }
你们都在说页面静态化, 它真的有那么神奇吗? 其实也没有那么神奇, 说白了吧,传统的网页上的数据是经过模板引擎渲染上去的,(好比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绝对飙升几个数量级
说一下市面上常见的静态资源优化技术吧:
另外当多个用户并发修改库存时,居然将库存修改为了负数, 自己使用的数据库引擎是 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 赐我白日梦, 欢迎点赞支持