1.Redis 服务器 can not get resource from pool.
1000 个线程并发还能跑, 5000 个线程的时候出现这种问题, 查后台 debug 日志, 发现 Redis 线程池不够. 刚开始设置的是:
- # Redis 配置文件
- #Redis
- Redis.host=127.0.0.1
- Redis.port=6379
Redis.timeout=300 等待时间 10s 改为 300s
Redis.password=123456
Redis.poolMaxTotal=1000 连接数, 刚开始最大连接数 设置为 100.
Redis.poolMaxIdle=500 最大空闲连接数 100 改成 500
Redis.poolMaxWait=300
顺便也改了一下 jdbc 的连接池参数, 最大空闲和最大连接数都改成 1000. 在测一下. 可以
- spring.datasource.filters=stat
- spring.datasource.maxActive=1000
- spring.datasource.initialSize=100
- spring.datasource.maxWait=60000
- spring.datasource.minIdle=500
- spring.datasource.timeBetweenEvictionRunsMillis=60000
- spring.datasource.minEvictableIdleTimeMillis=300000
- spring.datasource.validationQuery=select 'x'
- spring.datasource.testWhileIdle=true
- spring.datasource.testOnBorrow=false
- spring.datasource.testOnReturn=false
- spring.datasource.poolPreparedStatements=true
- spring.datasource.maxOpenPreparedStatements=20
2.5000 并发下的问题, 20 个商品, 库存减到 - 4980.
后来看代码发现, 判断库存用的是 if(stock==0 ) 抛出异常. 应该用 stock<0, 因为 若此时同时 2 个线程进来, 就永远小于 0, 后面的业务逻辑都可以执行.
3. 然后就是超卖的问题
第一次压力测试的时候, 5000 个线程, 分别取不同的 token(sessionId), 同时访问 秒杀这个接口, 商品个数只放了 20 个. 结果出现最后商品数量变负的问题.
4. 编码的问题
接口限流防刷的时候, 通过计数器限流, 如果超过某个阈值, 向前端返回一个 codeMsg 对象用于显示的时候, 显示的是 String 是乱码的问题, 之前由于一直返回都是 JSON 格式, 都是封装好在 data 里.
这次返回是直接通过输出流直接写到 response 直接返回字节数组的, 而不是 spring controller 返回数据(springboot 默认 utf-8), 出现乱码问题, 用 utf-8 编码, 解决.
5. 压测是如何压测的, 以及压测的瓶颈?
压测是利用 Jmeter 压测.(Apache 开发的基于 java 的压测工具).
压测具体实现:
1. 在数据库中提前插入 5000 个用户密码(脚本 for 循环 id 是 13000000+i), 密码统一为 "123456", 随机盐值也是固定的, 方便操作. 用 JDBC 存入数据库. 作为 5000 个备用用户.
2. 然后写了一个小脚本让 5000 个用户 post 请求我的登陆接口(login), 生成 sessionId 并存入缓存, 并改写了一下 login 接口让其换回 sessionId. 把这 5000 个用户的 id 和对应 sessionid 写到了一个 TXT 文件里面.
3. 最后利用 jmeter 创建 5000 个线程, 账号每个线程携带提前写好的用户 token(sessionId), 参数就是商品 id 和 sessionid, 商品 id 确定我要买的票是哪个, sessionid 用来获取用户信息.(从缓存中拿)
压测的瓶颈:
qps-126/s---- 静态化 - 250/s--- 接口优化 - 860/s.
瓶颈主要是对数据库的访问.
1. 数据库读取, 写入, 处理请求的速度.
数据库读取写入加上网络 IO 速度很慢, 减少对数据库的访问, 在缓存这一端就屏蔽掉大部分访问数据库的请求(Redis 预减库存操作)
2. 利用消息队列, 异步业务逻辑的处理速度慢, 可以先返回结果, 让其轮询.
3. 利用内存 map, 减少对 Redis 服务器的访问, flag 机制.
4. 其他想到的但还没实现
服务器系统的负载均衡 + 集群
数据库数据达到 1000W 以上就很慢, 分库分表
6. 用户登陆的整个流程是如何实现的?
1. 首先输入登陆页面的 url.[http://localhost:8080/login/to_login,controller 根据 map 映射返回给 html 页, 到达登陆页面]
2. 整个页面是一个 login 表单, 包含用户名和密码两个输入框部分, 还有一个登陆按钮和重置按钮.
3. 在前端, 给登陆按钮绑定一个 login()方法, login()方法中会获取表单中的用户名和密码, 然后将密码利用封装好的 md5()函数以及设置的固定盐值进行拼接, 盐值设置为 "1a2b3c", 然后进行 MD5 算法生成 4 个 32 位拼接的散列值作为输入密码(用于 网络传输), 作为参数传给后端.(这里的目的主要是第一道加密, 防止 http 明文传输, 泄漏密码).
4. 然后 Ajax 异步访问 do_login 接口, 参数为用户名和 md5 之后的密码, 后端接收到前端传输来的参数后, 会对用户名和密码进行参数校验, 验证是否为空, 是否有格式问题(密码长度 6 位以上, 用户名格式 11 位等等), 如果验证不通过, 返回 CodeMsg(), 封装好的对应的错误信息给前端.
5. 如果验证成功, 进入下一步, 用户的登陆, 首先通过用户名取用户对象信息(先从缓存中取, 取不到取数据库取, 取到了将用户信息存入缓存中, 下一次登录我们可以先从缓存中取用户, 降低数据库压力), 然后返回一个 user 对象, 再判断这个 user 对象是否为空, 若是空就抛出异常, 不是空的情况说明数据库中有该用户, 然后根据传入的密码和数据中保存的随机盐值, 进行 md5 再次拼接, 获得的值若是和数据库中的密码一致, 那么说明登陆成功.
关键点: 6. 登陆成功的时候, 随机生成 uuid 作为 sessionId, 将其写入 cookie 中返回给客户端, 并且将模块前缀 + 该用户 id 作为 key 和 sessionId 作为值, 存入缓存(这里为分布式缓存提供的基础). 这时候跳转到 抢票列表页面, 如果密码不匹配, 抛出异常, 返回.
7. 秒杀的两个关键点如何应对 -- 高并发应对策略 + 页面加载速度?
短时间的大访问量 网站服务器 同网站, 不同项目部署,/ 独立域名 避免对网站造成影响 高并发问题, 不停刷新 数据库 页面静态化
同网站, 不同项目部署,/ 独立域名 避免对网站造成影响 宽带 同网站, 不同项目部署,/ 独立域名 避免对网站造成影响 不能提前下单 服务器 url 动态化,+ 随机数
下单之后的抢的问题 sql 乐观锁
大量访问高并发的应对(主要访问大量访问数据库崩溃)
1.Redis 预减库存减少数据库访问
2.map 标记减少 Redis 访问屏蔽一定的请求减轻缓存压力
3. 消息队列异步处理
流量削峰 开始抢购的瞬间 大量并发进入, 先将请求入队, 若队列满了, 那么舍弃再入队的请求返回一个异常
先给前端一个数据返回表示排队中, 再进行后续的业务处理, 前端轮询最后成功或者失败在显示业务结果
4. 数据库运行的问题, 传统的 sql 写成存储过程(直接调用), 加速 sql
5. 数据库里锁及唯一索引来处理抢的问题.
页面加载速度
页面静态化, 缓存在客户端
CDN 服务器
在上表中列出来的解决方案中看出, 利用 页面静态化, 数据静态化, 反向代理 等方法可以避免 带宽和 sql 压力 , 但是随之而来一个问题, 页面抢单按钮也不会刷新了, 可以把 JS 文件单独放在 JS 服务器上, 由另外一台服务器写 定时任务 来控制 JS 推送.
另外还有一个问题, JS 文件会被大部分浏览器缓存, 我们可以使用 xxx.JS?v = 随机数 的方式来避免 JS 被缓存
8. 页面静态化的过程
更为激进的缓存方式(之前可以用将 HTML 源码缓存起来再读, 避免服务器渲染 HTML 过程).
什么是浏览器缓存:
简单来说, 浏览器缓存就是把一个已经请求过的 web 资源 (如 HTML 页面, 图片, JS, 数据等) 拷贝一份副本储存在浏览器中. 缓存会根据进来的请求保存输出内容的副本. 当下一个请求来到的时候, 如果是相同的 URL, 缓存会根据缓存机制决定是直接使用副本响应访问请求, 还是向源服务器再次发送请求. 比较常见的就是浏览器会缓存访问过网站的网页, 当再次访问这个 URL 地址的时候, 如果网页没有更新, 就不会再次下载网页, 而是直接使用本地缓存的网页. 只有当网站明确标识资源已经更新, 浏览器才会再次下载网页.
页面静态化的好处:
我们知道浏览器会将 HTML, 图片等静态数据, 缓存到本地, 在高并发抢票场景, 用户会通过不断的刷新页面来进行抢票操作, 这样带来 Web 带宽的浪费以及服务器的访问压力. 于是, 我们可以通过将抢票页面做成静态页面 HTML 页, 其中的票务数据通过 Ajax 异步调用接口来获取, 仅仅交互的是部分数据, 减少了带宽, 也加快用户访问的速度.
- function getDetail() {
- var goodsId = g_getQueryString("goodsId");
- $.Ajax({
- url : "/goods/to_detail/"+goodsId,
- type : "GET",
- success: function (data) {
- if (data.code == 0) {// 访问后端 detail 接口拿到数据
- render(data.data);// 渲染界面的方法
- }else {
- layer.msg(data.msg)
- }
- },
- error:function () {
- layer.msg("客户端请求有误!")
- }
- })
- }
- function render(detail) {
- var goodsVo =detail.goodsVo;
- var miaoshaStatus =detail.miaoshaStatus;
- var remainSeconds =detail.remainSeconds;
- var user =detail.user;
- if (user) {
- $("#userTip").hide();// 没有就不展示
- }
- // 用获取的参数 放入 对应的模板中
- $("#goodsName").text(goodsVo.goodsName);
- $("#goodsImg").attr("src", goodsVo.goodsImg);
- $("#startTime").text(new Date(goodsVo.startDate).format("yyyy-MM-dd hh:mm:ss"));
- $("#remainSeconds").val(remainSeconds);
- $("#goodsId").val(goodsVo.id);
- $("#goodsPrice").text(goodsVo.goodsPrice);
- $("#miaoshaPrice").text(goodsVo.miaoshaPrice);
- $("#stockCount").text(goodsVo.stockCount);
- countDown();// 调用倒计时
- }
- function countDown() {
- var remainSeconds = $("#remainSeconds").val();
- // var remainSeconds = $("#remainSeconds").val();
- var timeout;// 定义一个 timeout 保存 Timeout 值
- if (remainSeconds>0){// 秒杀未开始
- $("#buyButton").attr("disabled",true);/* 还没开始的时候按钮不让点 */
- $("#miaoshaTip").HTML("秒杀倒计时:"+remainSeconds+"秒");
- /* 且做一个倒计时 */
- timeout=setTimeout(function () {//setTimeout 为时间到了之后执行 该函数
- $("#countDown").text(remainSeconds-1);// 将显示中的值 -1
- $("#remainSeconds").val(remainSeconds-1);// remianSeconds 值减一
- countDown();// 在调用该方法 实现循环
- },1000)
- }else if (remainSeconds == 0){// 秒杀进行中
- $("#buyButton").attr("disabled",false);
- // 当 remainSeconds =0
- clearTimeout(timeout);// 取消 timeout 代码执行
- $("#miaoshaTip").HTML("秒杀进行中!")// 修改其中的内容
- /** 加入秒杀数学验证码 功能
- * 1. 一开始图形验证码和输入框都是隐藏的
- * 2. 当秒杀进行的时候, 显示验证码和输入框
- * */
- $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());// 访问验证码接口
- $("#verifyCodeImg").show();
- $("#verifyCode").show();
- } else {// 秒杀结束
- $("#buyButton").attr("disabled",true);
- $("#miaoshaTip").HTML("结束!!!")// 修改其中的内容
- }
- }
做法: 首先将票务详情这个 template 模板 HTML 页放在 static 文件下, 然后改掉 thymeleaf 模板语言标签让其成为纯 HTML 语言, 然后将票务列表中的链接指向(本来是 requestMapping, 向后端 contrller 请求这个详情业务及数据, 然后利用 spring 渲染模板, 在返回的), 现在直接指向 static 文件下的票务详情页(链接中带商品 id 作为参数), 最后在这个 HTML 页面写 Ajax 异步访问后端接口 / getdetail, 后端接口也改造一下返回的是这个商品的全部详细信息, 封装在 data 里, 以 JSON 的形式, 然后写了一个 render(), 把从后端传来的数据写进对应数据中.
- /** 页面静态化: 商品详情页面
- * 方法: 返回的是一个静态 HTML 页面 + 利用 Ajax(通过接口)从服务端获取对应数据 + JS 技术将数据放入 HTML
- * */
- @RequestMapping(value = "/to_detail/{goodsId}") // 前端传入的参数 goodsId
- @ResponseBody
- public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user,
- @PathVariable("goodsId") Long goodsId){// 通过注解 @PathVariable 获取路径参数
- /* 先将 user 传进去 用来判断是否登录 */
- model.addAttribute("user",user);
- /* 根据传入的 Id 通过 service 拿到对应的 Good 信息 */
- GoodsVo goods = goodsService.getGoodsById(goodsId);
- model.addAttribute("goods",goods);
- long startTime = goods.getStartDate().getTime();
- long endTime = goods.getEndDate().getTime();
- long nowTime = System.currentTimeMillis();/* 拿到现在时间的毫秒值 */
- /** 这里要做一个秒杀时间的判断 秒杀开始 秒杀结束 秒杀进行
- * */
- int miaoshaStatus = 0;/* 用该变量来表示 秒杀的状态 0 表示秒杀未开始 1 开始 2 结束 */
- int remainSeconds = 0; /* 表示剩余时间 距离秒杀开始的时间 */
- if (nowTime<startTime){// 秒杀未开始
- miaoshaStatus = 0;
- remainSeconds = (int)((startTime-nowTime)/1000);// 注意此时是 毫秒值 要除以 1000
- }else if (endTime<nowTime){// 秒杀结束
- miaoshaStatus = 2;
- remainSeconds = -1;
- }else {// 秒杀进行中
- miaoshaStatus = 1;
- remainSeconds = 0;
- }
- model.addAttribute("remainSeconds",remainSeconds);
- model.addAttribute("miaoshaStatus",miaoshaStatus);
- /*
- 将我们需要的数据 封装到 GoodsDetailVo 中
- */
- GoodsDetailVo goodsDetailVo = new GoodsDetailVo();
- goodsDetailVo.setGoodsVo(goods);
- goodsDetailVo.setMiaoshaStatus(miaoshaStatus);
- goodsDetailVo.setRemainSeconds(remainSeconds);
- goodsDetailVo.setUser(user);
- return Result.success(goodsDetailVo);
来源: http://www.jianshu.com/p/44957748218b