秒杀相信大家都不陌生,商家会发布一些价格低廉、数量很少的商品,吸引用户抢购,例如每年双十一活动就属于典型的秒杀活动。还有类似春节12306抢票、小米手机限量发售等都可以理解为“秒杀”。
秒杀特点是持续时间短,抢购人数多,参与人数大大高于商品数量。抢购开始前后大量用户请求涌入,极易给服务造成巨大压力。如果系统设计不当,还容易造成超卖、数据丢失等问题。
本文我们主要讨论在秒杀的高并发场景下,传统订单架构存在的性能瓶颈,如何利用redis、MQ等中间件对系统做优化,解决缓存加速、防止重复提交、排队下单、超卖、少卖、削峰、异步下单等核心问题。
秒杀业务流程简介
秒杀总体业务流程可以简述为
- 商户创建秒杀活动,设定秒杀时间段,选择本次活动的商品,设置折扣、库存等;
- 用户APP端在活动即将开始时会看到秒杀活动列表,点击活动可以看到商品列表,点击商品可以查看秒杀商品详情;
- 商品详情页用户点击立即抢购;
- 如果库存充足,则创建订单成功;否则秒杀失败
- 提交订单后超时未支付,系统会自动关闭订单,回滚库存。
秒杀页面主要分为:
(1)首页秒杀活动列表
(2)商品详情页
普通订单系统
我们来看看普通订单系统是如何处理订单请求的.
订单下单流程图
流程分析
在springcloud环境下,普通订单下单流程可以总结为:
1.用户确认订单、提交订单,发送下单请求至订单微服务;
2.订单服务会调用用户服务做一系列业务校验,如账号是否异常等;
还会调用商品服务,校验商品信息;
商品服务又会调用活动服务,校验优惠券、计算优惠等;
3.各服务从MySql获取业务数据,进行业务计算、业务校验;
4.生成订单,最后将订单数据入库。
瓶颈分析
普通订单系统分析
以上是传统微服务架构订单业务的经典流程,在用户量不多、并发不高的正常业务场景下,支撑起正常的业务需求是没问题的。可以通过部署集群、数据库分库分表和读写分离、sql调优、硬件升级等方式,进一步提高系统稳定性和抗并发能力。
但是对于秒杀业务场景,由于秒杀活动特点是商品库存少,参与人数多,在秒杀开始前后,系统的瞬间请求流量飙升,对后端服务尤其是数据库造成很大压力,如果不能进行有效削峰、限流,所有请求一次性到某一台服务器或数据库上,服务很有可能出现卡顿、不可用甚至宕机的可能,给用户造成不良体验。
普通订单系统处理秒杀业务的瓶颈
数据库负担过重
从上图可以看出,仅一个下单请求,所有服务的查询、修改等都是直接操作mysql,没有用到缓存,秒杀开始,系统瞬间承受平时数十倍甚至上百倍的流量,导致mysql cpu占用升高,压力过重,直接拖慢所有系统服务。
频繁的跨服务调用
由上图可以看出,秒杀相关接口在查询业务数据时,由于下单业务复杂,需要校验的业务项非常多,后端不得不频繁跨服务调用,订单服务会调用商品、用户等服务、活动等服务,活动服务可能还会调用其它服务,
调用链过多、过长,可能某一环节响应时间过长而拖慢系统整体速度,同时微服务之间的互相调用也会占用系统CPU、内存资源,造成服务器性能下降。
容易产生大量无效下单请求
秒杀商品只有10个,却有1000个下单请求,这1000个请求到后端会全部走一遍下单逻辑,而实际上真正成功的订单只有10个,其它秒杀失败的请求没有过滤掉。
没有排队处理请求
所有请求一窝蜂涌入,容易造成请求积压,造成OOM。
串行处理
在高并发情况下,为保证不出现超卖问题,所有涉及库存操作都会加锁处理,串行执行,增加请求处理耗时。即使系统能容忍很高的并发,也很可能出现请求堆积、超时等情况。
链接暴露
秒杀url很容易通过抓包工具获取,竞争对手或黄牛党可以通过脚本或刷单工具发送下单请求,轻则活动还没开始商品便卖光,严重的服务器宕机,活动失败,GG。
秒杀常见优化方案
关于秒杀系统,可优化的点非常多,这里列出如下几点:
前端层面
前端优化(前端按钮点击频率限制、限制用户维度访问频率、限制商品维度访问频率、验证码机制等)
页面数据的静态化+多级缓存(CDN加速+Nginx+Redis)
服务层面
web服务器优化(tomcat、undertow)
nginx限流
负载均衡
服务器硬件升级
削峰处理
服务降级、熔断
jvm性能调优
业务层面
数据库分库分表、读写分离
sql调优
代码调优
.................
本次优化关键点
实际上,受限于经费、时间、团队技术水平等条件,实际优化中我们可能无法对以上几点逐条优化,一是耗时耗力,二是可能没必要,具体优化时还是要以实际业务并发量为准。 在资源、时间有限的情况下,我们需要一个高效、最能够显著提升效果的优化方案。
本文主要介绍在服务层面,如何针对瞬时的高并发请求做削峰处理;业务层面,如何利用缓存减轻mysql数据库访问压力、如何排队处理、如何防止重复提交、防止超卖问题等。
利用缓存
秒杀的业务特点是读多写少,一个秒杀商品只有10个,可能有10w个人来抢,最终只有10个用户会产生写操作,其它请求都是查询库存,非常适合利用缓存优化。
缓存这块我们选用redis,redis基于内存,内存的读写速度非常快;同时redis内部是单线程操作,省去了很多上下文切换线程的时间。redis采用多路复用技术,非阻塞式IO,可以抗住高达百万级的并发量。
排队下单
利用redis进行排队抢单,记录排队数据。秒杀请求到后端后,不立即走创建订单逻辑,先通过redis校验排队、库存信息,校验通过后将秒杀请求缓存到redis。
削峰处理
通过RabbitMQ消息队列削峰:
秒杀请求不直接生成订单,先存入MQ消息队列,可以写一个消息监听器,平缓消费秒杀请求数据,减轻数据库并发量。
优化方案设计
数据缓存
通过以上分析我们知道秒杀的最大瓶颈便是mysql,所以我们要将mysql的压力转移给缓存。
在活动开始前,我们可以配置循环定时任务,将秒杀活动、秒杀商品相关信息全部缓存到redis中,可以根据活动信息,设置缓存的失效时间。
前端秒杀活动、商品详情等数据的获取,全部走redis。
确认订单
用户发送确认订单请求时,首先校验该用户是否是否已经排队,排队信息从redis中获取,如果已经排队下单,直接返回; 否则继续走确认订