秒杀系统压力测试及项目优化(二)

压力测试一:只使用mysql的压力测试

测试商品列表接口结果:一秒只支持147.7线程

image-20210316203738249

image-20210316204042228

测试秒杀接口时,QPS与之前差不多,但是出现了超卖现象。说明单用户的判断逻辑在多用户场景下不适用,测试模拟了5000个用户同时秒杀一个10库存的商品,最终超卖了13个,这在更大规模用户秒杀商品时几乎是致命错误。

优化项目

优化一:页面缓存+URL缓存+对象缓存

页面缓存

  1. 取缓存
  2. 如果缓存不存在,手动渲染模板
  3. 结果输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RequestMapping(value="/to_list",produces="text/html")
@ResponseBody
public String list(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user) {
model.addAttribute("user", user);

//取缓存
String html = redisService.get(GoodsKey.getGoodsList,"",String.class)
if(!StringUtils.isEmpty(html)){
return html;
}

//如果为空则,查询商品列表
List<GoodsVo> goodsList = goodsService.listGoodsVo();
model.addAttribute("goodsList", goodsList);
// return "goods_list";


SpringWebContext ctx = new SpringWebContext(request,
response,request.getServletContext(),
request.getLocale(),model.asMap(),applicationContext);

//手动渲染
html = thymeleafViewResolver.getTemplateEngine().process("goods_list",ctx);
//如果模板不是空,保存到缓存中去
if(!StringUtils.isEmpty(html)){
redisService.set(GoodsKey.getGoodsList,"",html);
}
return html;
}

此处遇到问题:Sping5中SpringWebContext方法过时

因为在thymeleaf.spring5的API中把大部分的功能移到了IWebContext下面,用来区分边界。剔除了ApplicationContext 过多的依赖,现在thymeleaf渲染不再过多依赖spring容器

调用这个即可

1
2
IWebContext ctx =new WebContext(request,response,
request.getServletContext(),request.getLocale(),model.asMap());

优化二:页面静态化,前后端分离

弃用thymeleaf,使用原生Html

采用静态化页面

1
2
3
4
5
6
7
8
9
#需要在配置中加上如下内容,否则会404
#static
spring.sources.add-mappings=true
spring.sources.cache-period= 3600
spring.sources.chain.cache=true
spring.sources.chain.enabled=true
spring.sources.chain.gzipped=true
spring.sources.chain.html-application-cache=true
spring.sources.static-locations=classpath:/static/

解决超卖:

1、数据库代码修改

原代码

1
2
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId}")
public int reduceStock(MiaoshaGoods g);

加上判断条件 and stock_count > 0,修改后

1
2
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0")
public int reduceStock(MiaoshaGoods g);

但是这样修改后,仍然不能防止一个用户秒杀到两个商品的情况:一个用户同时发起两个请求并且同时秒杀到商品的情况。和一个用户不能秒杀两个商品的规则相违背

2、所以需要利用数据库的索引,利用唯一索引防止用户插入重复数据,数据库添加索引如下:

image-20210319164309659

以上两步可以完全杜绝商品卖超的情况

总结:1、数据库加唯一索引,防止用户重复购买。2、SQL加库存数量判断:防止库存变成负数。

优化三:静态资源优化

1、JS/CSS压缩,减少流量

2、多个JS/CSS组合,减少连接数

tengine.taobao.org

优化四:CDN优化

CDN就近访问

优化五:接口优化

主要工作:

1、Redis预减库存减少数据库访问

2、内存标记减少Redis访问

3、请求先入队缓冲,异步下单,增强用户体验

思路:减少数据库访问

1、系统初始化,把商品库存数量加载到Redis

2、收到请求,Redis预减库存,库存不足,直接返回,否则进入3

3、请求入队,立即返回排队中

4、请求出队,生成订单,减少库存

5、客户端轮询,是否秒杀成功

使用rabbitmq

引入依赖

1
2
3
4
<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#rabbitmq
spring.rabbitmq.host=121.4.60.79
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
#\u6D88\u8D39\u8005\u6570\u91CF
#消费者数量
spring.rabbitmq.listener.simple.concurrency= 10
spring.rabbitmq.listener.simple.max-concurrency= 10
#每次取几个
spring.rabbitmq.listener.simple.prefetch= 1
#\u6D88\u8D39\u8005\u81EA\u52A8\u542F\u52A8
#消费者是否自动启动
spring.rabbitmq.listener.simple.auto-startup=true
#消费失败后是否重新压入
spring.rabbitmq.listener.simple.default-requeue-rejected= true
#以下都是重试配置,如果满了则每隔1000
spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1000
spring.rabbitmq.template.retry.max-attempts=3
spring.rabbitmq.template.retry.max-interval=10000
#如果是2则第一次等1秒第二次X2等两秒,第三次等4秒
spring.rabbitmq.template.retry.multiplier=1.0