一、秒杀的概念
秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,会吸引大量用户前来抢购,并且会在约定的时间点同时在秒杀页面进行抢购。
二、秒杀系统场景特点
瞬时高并发:秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。联想到小米手机的抢购场景,在小米手机抢购的场景一般都会有10w+的用户同时访问一个商品页面去抢购手机。
请求数远大于库存数:请求远远大于秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
业务流程简单:秒杀业务流程比较简单,1、配置秒杀活动页(倒计时页面);2、查询商品详情(点击马上抢);3、点击购买,校验库存 ;4、扣库存,创建订单;5、去支付。
三、概要设计
四、详细设计
基于梳理的秒杀的流程,那我们按照流程一一分析,各个环节怎么去设计?
4.1、配置秒杀活动页
1)对于大促时候的秒杀活动,一般运营会配置静态的活动页面,配置静态活动页面主要有两个目的一方面是为了便于在各种社交媒体分发,另一方面是因为秒杀活动页的流量是大促期间最大的,通过配置成静态页面可以将页面发布在公有云上动态的横向扩展;
2)将秒杀活动的静态页面提前刷新到CDN节点,通过CDN节点的页面缓存来缓解访问压力和公司网络带宽,CDN上缓存js、css和图片;
3)将活动H5页面部署在公有云的web server上,使用公有云最大的好处就是能够根据活动的火爆程度动态扩容而且成本较低,同时将访问压力隔离在公司系统外部;
4)在提供真正商品秒杀业务功能的app server上,需要进行交易限流、熔断控制,防止因为秒杀交易影响到其他正常服务的提供,我们在限流和熔断方面使用了hystrix,在核心交易的controller层通过hystrix进行交易并发限流控制,当交易流量超出我们设定的限流最大值时,会对新交易进行熔断处理固定返回静态失败报文。
4.2、查询商品详情(点击马上抢进入商品详情)
用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。
解决方案:重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,用户请求不需要经过应用服务。
突然增加的网络及服务器带宽
假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G(200K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽。
直接下单
秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。
解决方案:为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。办法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到。
如何控制秒杀商品页面购买按钮的点亮
购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的。如果该页面是动态生成的,当然可以在服务器端构造响应页面输出,控制该按钮是灰色还 是点亮,但是为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计为静态页面,缓存在CDN、反向代理服务器上,甚至用户浏览器上。秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。
解决方案:使用JavaScript脚本控制,在秒杀商品静态页面中加入一个JavaScript文件引用,该JavaScript文件中包含 秒杀开始标志为否;当秒杀开始的时候生成一个新的JavaScript文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是,加入下单页面的URL及随机数参数(这个随机数只会产生一个,即所有人看到的URL都是同一个,服务器端可以用redis这种分布式缓存服务器来保存随机数),并被用户浏览器加载,控制秒杀商品页面的展示。这个JavaScript文件的加载可以加上随机版本号(例如xx.js?v=32353823),这样就不会被浏览器、CDN和反向代理服务器缓存。
这个JavaScript文件非常小,即使每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽造成太大压力。
如何只允许第一个提交的订单被发送到订单子系统
由于最终能够成功秒杀到商品的用户只有一个,因此需要在用户提交订单时,检查是否已经有订单提交。如果已经有订单提交成功,则需要更新 JavaScript文件,更新秒杀开始标志为否,购买按钮变灰。事实上,由于最终能够成功提交订单的用户只有一个,为了减轻下单页面服务器的负载压力, 可以控制进入下单页面的入口,只有少数用户能进入下单页面,其他用户直接进入秒杀结束页面。
解决方案:假设下单服务器集群有10台服务器,每台服务器只接受最多10个下单请求。在还没有人提交订单成功之前,如果一台服务器已经有十单了,而有的一单都没处理,可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面,再刷新一下页面,有可能被一单都没有处理的服务器处理,进入了填写订单的页面,可以考虑通过cookie的方式来应对,符合一致性原则。当然可以采用最少连接的负载均衡算法,出现上述情况的概率大大降低。
4.3、校验库存
校验库存是查询请求,利用缓存应对写请求:缓存也是可以应对写请求的,比如我们就可以把数据库中的库存数据转移到Redis缓存中,所有减库存操作都在Redis中进行,然后再通过后台进程把Redis中的用户秒杀请求同步到数据库中。
检查商品是否有货,没有就直接返回,有就进入购买流程。我们发现检查库存走的是数据库。
这么大量的请求过来,走数据库肯定是不行的,想到的肯定是把库存放到缓存中,因为是分布式,所以放到Redis中。
那就有个问题了,什么时候把商品库存放到缓存中?
最好的方式就是通过后台管理系统,因为我们新建一个秒杀商品,肯定是通过后台管理系统操作的,添加秒杀商品后,应该会有一个类似按钮【上线发布】,可以在这个按钮事件进行缓存,当然小伙伴们可根据自己的业务来处理。
4.4、扣库存
由于参加秒杀的商品,一般都是“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用“下单减库存”更加合理。另外,理论上由于“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。
下单减库存在的问题:下单就把库存减了,如有竞争对手通过恶意的方式将商品全部下单,让商品库存为0了,但最后他是不会付款的。这样导致正常的用户购买不了,卖家也没有得到钱。
解决:
1)回收库存:就是让用户有半个小时支付时间,如超过不支付,就失效此订单,并且回收库存。
2)用户标记:加入风控系统,一般大中型平台都会有风控系统。也就是我们要监控哪些用户一直是下单不付款的,如果超过一定的阀值,就给这个用户打上标记,归属黑名单。
3)限制购买数量:在商品限制购买数量,经常使用的是每个用户限制购买1~2件,也能规避恶意下单。
超卖问题:
如:我们现在的剩余库存为1,同时有2个线程请求过来,又同时执行到验证库存代码块,这样就都通过了验证,然后又先后执行了扣库存,但扣库存是直接减1的,导致库存变成了-1,超卖就此形成了。
解决:
由于库存并发更新的问题,导致在实际库存已经不足的情况下,库存依然在减,导致卖家的商品卖得件数超过秒杀的预期。方案:采用乐观锁
update auction_auctions set quantity =#in Quantity# where auction_id =#itemId# and quantity = #dbQuantity#
还有一种方式,会更好些,叫做尝试扣减库存,扣减库存成功才会进行下单逻辑:
update auction_auctions set quantity = quantity-#count# where auction_id =#itemId# and quantity >= #count#
4.5、项目部署
对现有网站业务造成冲击
秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。
解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离,比如在现有后台将秒杀页面进行配置,对应的连接是请求另外一台专门的秒杀服务器。