最近,公司一直在搞秒杀,当然,过程中还是出了很多问题,随便在confluence搜索一下都能看到。

我也来谈一下秒杀吧,当然,我自己谈这个话题也不专业,权当纸上谈兵吧。

什么是秒杀

在谈秒杀前,首先要搞清楚,什么是秒杀。

简单来说,秒杀就是在同一个时刻有大量的请求争抢购买同一个商品并完成交易的过程。

这个解释是对用户的。对技术人员来说,秒杀就是在同一时刻有大量的并发读和并发写

那么,按照这个思路来看一个好的秒杀系统具备的特征就应该是:

  • 在大量并发读的情况下,没挂,且响应很快。
  • 在大量并发写的情况下,没挂,且保证数据一致。

所以,本质上来说秒杀系统就是一个满足大并发、高性能和高可用的分布式系统

下定义都是很简单的,但是做起来确是很难的。因为并发问题,通常都是困扰程序员的一大难题。

那么,不管是哪种技术平台,哪种架构方式,我认为在设计秒杀系统的时候,可能都需要考虑以下一些设计原则:

  • 数据尽量少:不管是用户请求的数据,还是服务器回传的数据,都要尽量少。因为数据量大了,首先占用网络带宽,同时网络传输时间也会加长。再一个,网络传输通常都是加密的,数据量大,CPU占用也会增加。
  • 请求数尽量少:请求数多了,对socket的占用是非常大的,这会带来两个问题:如果是客户端请求,那么通常请求数会受浏览器限制;如果是内部服务间的请求,大量的TCP建立,三次握手,开销也很大。
  • 路径尽量短:路径是指从客户请求开始,到接到响应,中间经过的环节,包括经过了netscaler,api gateway,各个微服务等。每个环节都有可能不稳定,所以,多一个环节就多一个不确定因数。
  • 依赖尽量少:这个就不用多解释了,如果一个秒杀系统依赖的系统或者服务过多,那么拖垮这个系统的有可能是一些不必要的依赖。

好,接下来就尝试讨论一下如何来做好秒杀。

这一章的标题叫做分,意味着我们要尝试分离一些东西。

我们现在的秒杀就是在已有的系统上进行的,只是这些秒杀商品是在特定的时间才能看到库存而已,当库存卖完了也就结束了。

但是随着访问量的增加,瓶颈很快就会显现,我们需要进行有针对性的架构优化。但是既然是有针对性的,我们可能就无法在现有系统上来做,因为现有系统可能是一个庞大的,依赖关系很多的复杂产物。按照我上一篇提到的几个原则,很可能是不满足的。

所以,第一个要分的就是:把秒杀系统分离出来单独打造一个系统,这样可以有针对性地做优化。并且在系统部署上也独立做一个集群。这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载。

有了独立系统后,我们就可以大刀阔斧的优化了,优化方式可以参照上一篇提到的原则,例如尽量少请求,少传输数据等。

第二个要分离的就是动静分离。这个比较好理解,静态内容比如html,js,css等等,这些内容要放得离用户越近越好。常见的位置,例如浏览器里、CDN和服务端缓存。

第三个要分离的就是冷热数据。秒杀商品是一个典型的热点商品,它们在很短时间内被大量用户执行访问、添加购物车、下单等操作,这些操作我们就称为“热点操作”。因为是我们自己在掌控秒杀,所以热点商品是可以提前预知的,那么对于热点数据就要做提前的缓存,能不去操作数据库的就不要去操作,甚至库存扣减都可以放到缓存里来做。

削什么?看下图:

这是前两天秒杀的流量监控图,你会发现请求在秒杀开始的瞬间增加的很多。这是因为秒杀请求在时间上高度集中于某一特定的时间点。这样一来,就会导致一个特别高的流量峰值,它对资源的消耗是瞬时的。

但是对秒杀这个场景来说,最终能够抢到商品的人数是固定的,也就是说100人和10000人发起请求的结果都是一样的,并发度越高,无效请求也越多。

但是从业务上来说,秒杀活动是希望更多的人来参与的,也就是开始之前希望有更多的人来刷页面,但是真正开始下单时,秒杀请求并不是越多越好。因此我们可以设计一些规则,让并发的请求更多地延缓,而且我们甚至可以过滤掉一些无效请求。

这就是削峰的意义所在:一是可以让服务端处理变得更加平稳;二是可以节省服务器的资源成本针对秒杀这一场景。削峰从本质上来说就是更多地延缓用户请求的发出,以便减少和过滤掉一些无效请求。

怎么削?

排队

要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,,中问通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。在这里,消息队列就像“水库”一样,拦著上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。

实际上,排队就是把“一步的操作”变成“两步的操作”,其中增加的一步操作用来起到缓冲的作用。但是呢,这会引入新的调用环节,不符合“请求尽量少”的原则,所以这是为了不让系统奔溃的一个妥协。

答题

答题实际上就是我们俗称的“验证码”,例如:

这主要是为了增加购买的复杂度,从而达到两个目的。

第一个目的是防止部分买家使用机器人在参加秒杀时作弊。所以系统增加了答题来限制机器人。增加答题后,下单的时间基本控制在2s后,机器人的下单比例也大大下降。

第二个目的其实就是延缓请求,起到对请求流量进行削峰的作用,从而让系统能够更好地支持瞬时的流量高峰。这个重要的功能就是把峰值的下单请求拉长,从以前的1s之内延长到2s-10s。这样一来请求峰值基于时间分片了。这个时间的分片对服务端处理并发非常重要,会大大减轻压力。而且,由于请求具有先后顺序,靠后的请求到来时自然也就没有库存了,因此根本到不了最后的下单步骤,所以真正的并发写就非常有限了。

边缘计算

理论上我们一定会用上 CDN。然后,我们还需要在这些 CDN 结点上做点小文章。

一方面,我们需要把小服务部署到 CDN 结点上去,这样,当前端页面来问开没开始时,这个小服务除了告诉前端开没开始外,其还会做一下有多少人在线的一个统计。每个小服务会把这当前在线等待秒杀的人数每隔一段时间就回传给我们的数据中心,于是我们就知道全网总共在线的人数有多少。

假设,我们知道有大约 100 万的人在线等着抢,那么,在我们快要开始的时候,由数据中心向各个部署在 CDN 结点上的小服务上传递一个概率值,比如说是 0.02%。于是,当秒杀开始的时候,这 100 万用户都在点下单按钮时,首先请求到的是 CDN 上的这个小服务,这个小服务按照 0.02% 的量把用户放到后面的数据中心,也就是 1 万个人放过去两个,剩下的 9998 个都直接返回秒杀已结束。

于是,100 万用户被放过了 0.02% 的用户,也就是 200 个左右,而这 200 个人在数据中心抢那 100 个 iPhone,也就是 200 TPS,这个并发量怎么都应该能扛住了。

做一个秒杀系统,最重要的一点就是:别超卖

所以,扣减库存不是一个简单的事情,不能卖多,也不能卖不完。这其中有一个问题很关键:什么时候扣减库存?你说系统是用户下单了就算这个商品卖出去了,还是等到用户真正付款了才算卖出了呢?

下面我们就来看看这其中的区别,当然,这篇应该让 Eric Tan来写的,他应该是专家哈,hot item就是干这个事情的。我先班门弄斧了。

在正常的电高平台购物场景中,用户的实际购买过程一般分为两步:下单和付款。你想买一台iphone 手机,在商品页面点了“立即购买”按钮,核对信息之后点击“提交订单”,这一步称为下单操作。下单之后,你只有真正完成付款操作才能算真正购买,也就是俗话说的“落袋为安”

那如果你来设计系统,你会在哪个环节完成减库存的操作呢?总结来说,减库存操作一般有如下几个方式:

  • 下单减库存,即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。所以,如果有人恶意下单,下完后不付款,会导致无法正常销售商品。
  • 付款减库存,即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。所以,并发高的时候会出现超卖。
  • 预扣库存,这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如10分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。

针对这三种方式,秒杀应该用哪种扣减呢?我认为应该用下单减库存。原因是,因为不能超卖,所以付款减不适合。另外,秒杀可能时间很短就结束了,用预扣没必要,等保留时间过了,估计秒杀都完了。

我们可以用一些业务手段或者技术手段来过滤恶意下单,所以,综合来看还是应该用下单减库存。

剩下就是怎么扣的问题,秒杀商品的库存绝对是一个热点数据,所以能在缓存扣就一定不要去数据库扣

如果万不得已一定要去数据库扣,怎么破?削峰,把大部分请求过滤掉,大大降低最后去数据库扣减的量。

小结

好,简单探讨了一下秒杀的一些东西,写得有点乱,希望你没看晕。

当然,是不是做好了这几点就一定能做一个成功的秒杀?答案是不一定,因为没人可以预判到真实的情况,所以我们一定需要有一套运维的方案来应对各种情况。

  • 降级:关闭一些系统的非核心功能,把资源留给更核心的业务去用。
  • 限流:限流是为了保护系统不奔溃,让一些请求失效。当然,前提是我们清楚的知道我们系统的承载量,这样设置的限流阈值才会更合理。
  • Plan B:如果以上措施都还是没有保护好我们的系统,最终还是崩了的话,我们需要有一个备用方案来兜底。