前言
各大电商平台的崛起以及对应的各种优惠抢购需求的出现,那么秒杀系统设计便是程序员必不可少的一种设计能力。现在我们假设一个场景,现在需要我们设计一个秒杀系统来华为Meta60 pro进行抢购,限购1000台,价格1元(这不得疯抢),那么我们该如何来进行设计呢?
核心问题
高并发
秒杀涉及大量的读写并发,时间极短、瞬间用户量大、QPS高,单靠DB是扛不住的,Redis虽然能够支撑十几万的并发,但是需要考虑到的点也是很多的,缓存雪崩、缓存击穿、缓存穿透等
高可用
我们能够容忍QPS提不上去,但是无法容忍系统不可用,并且秒杀业务不能影响到正常的电商业务,服务器也不能共用,域名也需要独立出来,当出现我们没有考虑到的问题时,需要有兜底方案
一致性
秒杀中商品库存扣减是非常重要的,如果出现了超卖问题,那么可能会导致赔本、用户投诉、品牌声誉受到影响等问题,而扣减库存又分为拍下扣减库存、付款扣减库存、预扣等情况,所以在高并发请求下,要保持数据一致性是一个不小的挑战
恶意请求
只要是抢购活动,那么就一定会有黄牛的出现,只要抢到转手一卖就能大赚一笔,那我搞几十台机器写个脚本,模拟出十几万人的请求,那么是否也有90%以上的几率能够抢购成功了
下单URL暴露
正常情况下,在秒杀活动开始前按钮都是置灰的,但是如果URL是静态的话,那么就有可能通过浏览器的开发者模式获取到对应的URL链接,如果URL链接暴露出去,那么会有很大的风险。
概要设计
前端
- 秒杀系统不同于一般的网购,参与秒杀的用户更关心的是更快的刷新商品页面并下单,所以秒杀系统的设计应该更加简洁,减少不必要的前端组件加载
- 商品开始前和秒杀商品卖出后,该按钮应该置灰,不可以点击,下单数量固定、送货地址以及付款方式使用用户默认值,若无默认值可以订单提交后修改。
- 浏览器尽可能在本地缓存当前页面,页面本身的HTML、JS、CSS、图片等都开启浏览器缓存,其次秒杀系统还需要使用CDN缓存,将静态资源缓存到CDN上面。
URL动态化
在秒杀商品静态页面中加入一个特殊的JavaScript文件,这个文件不被任何地方缓存,秒杀未开始时,该Js文件内容为空,当秒杀开始时,定时任务生成新的Js文件内容,并推送到JS服务器中,新的JS文件包含了秒杀是否开始的标志和下单页面URL的随机参数,当用户刷新时,新的JS文件被用户浏览器加载,根据Js中的参数控制秒杀按钮的点亮,提交表单的URL参数也是来自于这个JS文件,该JS文件非常小,不会对带宽产生大的影响
动静分离
- 实体机单机部署
- 统一Cache层
- CDN
流量削峰
对于秒杀场景来说,最终能够抢到商品的人数是固定的,也就是说1000人和10w人发起请求的结果都是一样的,并发度越高,无效请求也越多。 我们希望更多人的来参与,也就说在开始前可以有更多的人刷新页面,但真正开始下单的时候,秒杀请求并不是越多越好,我们需要过滤掉一些无效的请求以及让请求更多地延缓。 流量削峰主要分为无损(不会损失用户的发出请求)和有损两种方案,无损削峰主要有:排队、验证、分层过滤。有损削峰:限流和机器负载保护等措施。
排队
使用消息队列来缓冲瞬时流量,把同步调用换成异步调用,中间通过队列承接流量洪峰,从而达到削峰的效果。
但是如果流量峰值持续一段时间达到了消息队列的处理上限,本机的消息积压已经达到了存储空间的上限,那么队列也有压垮的风险,虽然保护了下游系统,但和直接丢弃也没有什么区别。 除了消息队列,类似的排队方式也有很多:
- 利用线程池加锁等待也是一种常用的排队方式
- 先进先出、先进后出等常用的内存排队算法的实现方式
- 把请求序列化到文件中,然后再顺序地读文件(例如基于MySQL binlog的同步机制)来恢复请求等方式。
验证
通过增加验证码、答题的方式能够有效杜绝部分买家使用秒杀器在参与秒杀活动时作弊,并且能够实现延缓请求,对请求流量进行削峰。使得请求峰值基于时间分片,大大减少服务器压力,由于请求有先后顺序,那么请求靠后到来时自然就没有库存了,大大降低了实际的并发量。
分层过滤
在现实的秒杀抢购中,有大量无效的请求,这个时候就需要通过分层过滤的方式去过滤掉这部分请求,分层过滤类似于漏斗的形式。 请求分别经过CDN、商品详情页面、订单系统和数据库
- 将动态请求获取商品详情等读请求在WEb端,通过缓存过滤
- 对读数据不做强一致性校验,减少一致性带来的性能瓶颈
- 对写请求根据时间,过滤掉过期或者请求时间不合理的请求
- 对写请求做限流保护,将超出系统承载能力的请求过滤掉
- 对写数据进行一致性校验,保留最终有效的请求
库存扣减
库存扣减分为下单减库存、付款减库存、预扣库存,其中付款减库存、预扣减库存会出现用户下单后无法购买商品的情况,针对于秒杀场景来说下单减库存更加合理。 如果库存数据不存在联动的情况,比如复杂的SKU库存和总库存的联动,那么可以将库存放到可持久化的Redis上进行操作,如果存在复杂的库存联动,那么只能使用事务,在数据库中进行库存操作。 主要有两种
- 应用层排队:按照商品维度设置队列顺序进行
- 数据库层排队:在数据库层中顺序执行