打破「公平」,让秒杀系统飞起来

坑爹的技术竞赛

几年前,公司组织了一次技术竞赛。自由组队,每组4人,36小时以内按要求完成设计、代码实现和文档。第一名奖金3万元。前端

一开始不是很想参加,由于要通宵熬夜(如今想一想,不参加竞赛是个明智之举)!不事后来仍是接收了其余部门的邀请,参赛了。可是,在比赛前一天,其余三我的放我鸽子!!!放我鸽子!!!放我鸽子!!!没办法,只能从新组队,最终两个后端、两个前端!可是,比赛当天,另外一个后端去参加歌唱比赛了!!!参加歌唱比赛了!!!参加歌唱比赛了!!!因此变成了一个后端+两个前端。咱们成了当时参赛小组里惟一的三人小组,惟一的有前端的小组,仍是两个前端!!!node

而后比赛的题目是:实现一个秒杀系统!!!web

  • 不能使用Java和Spring技术体系外的技术(由于除了咱们组,其余组都是Java后端,因此只对Java技术作了限制),若是使用要扣分
  • 吞吐量越高越好
  • 不能出现超卖或少卖的状况
  • 要有完善的文档
  • ......

当时咱们三个就想直接弃权了!可是转念一想,都走到这一步了,仍是试试吧!数据库

最终的结果是,咱们没完成(意不意外?惊不惊喜?)!!!前端在最后时间点才开发完成,测试的时间都没有!后来测试的时候,修复了几个bug,为了公平起见,就没有参与最后的比赛!后端

不过,在全部的参赛队伍中,咱们的吞吐量是最高的,其余队伍的TPS基本在三四千左右!咱们的TPS基本在一万七左右!若是一开始就完成了,基本秒杀其余组!浏览器

咱们的吞吐量是其它组的四倍,究其缘由是由于咱们的人员组成与其余组不一样,致使咱们的设计思路与其余组的设计思路也不一样缓存

在以前的什么是软件架构一文中,我对软件架构作了一个定义:「架构是特定约束下决策的结果」!服务器

打破「公平」,让秒杀系统飞起来

此次技术竞赛的经历,正好能够验证这一观点:即便是在技术不对口、人数不足等劣势条件下,只要作出合适的决策,依然能作出超出预期的架构设计!markdown

有时候,对于合适的架构设计,劣势有时候会成为优点架构

咱们先来看下,对于秒杀系统来讲,通常的设计思路。

秒杀系统通常设计思路

秒杀系统的特色是:

  • 瞬时请求量很高
  • 持续时间较短

因此秒杀系统须要解决的是「在高并发状况下,用户请求及数据更新的问题」!

通常的设计思路:

  • (变相)扩容
  • 提升性能

具体方式有:

动静分离

对于通常的应用来讲,请求流程大体以下:

  • 服务端接收到请求,从数据库中查询相应数据
  • 选择对应的展现模板
  • 经过模板和数据渲染出最终页面
  • 将页面返回给客户端

当访问量很大的时候,服务器压力会很是的大!解决方案就是动静分离

作软件开发的都知道要「将变化的内容和不变的内容隔离开」,以便于独立进化。这里其实也是同样的思路。

模板是个静态的内容,部署后通常是不会变化的;而数据是个相对动态的内容,根据请求参数的不一样,数据可能不一样。因此咱们须要将模板与数据分离。

之前的作法是后端事先生成渲染后的页面,缓存起来或直接部署到静态服务器或CDN,请求时直接从缓存(静态服务器/CDN)中获取页面,而动态数据经过AJAX请求的方式获取。服务器再也不须要渲染页面,只须要返回少许的数据便可。既下降了服务器压力,又减小了服务端数据的传输。

而如今很流行的先后端分离就能很容易的解决这个问题。页面独立部署,数据异步获取,页面渲染由浏览器负责。这里和普通的先后端分离还有些差别,须要将相对静态的数据都静态化,以减小动态数据量。

分离后,静态内容和动态内容就能够独立进化。例如静态内容能够部署到CDN上,用户能够从最近的服务器获取到数据。相对热点的动态数据能够作缓存,下降数据库压力,进一步提升服务端响应。

独立部署

「独立部署」其实也能够当作是一种「动静分离」。将秒杀系统这个相对动态的系统,和相对静态的业务系统分开部署

缘由很好理解,秒杀系统的请求量很大,可能会因为预估不足或系统问题,致使了秒杀系统的负载太高、响应变慢。若是秒杀系统是业务系统的一部分,则会致使业务系统响应变慢,甚至致使系统没有响应。且秒杀是个短时间活动也不是核心业务,而业务系统是须要长期稳定运行的。不能由于一个短时间非核心的活动,而影响了核心的业务系统。

因此秒杀系统最好和业务系统分开独立部署。即便秒杀系统挂了,也不会影响业务系统的正常对外服务。

一样的道理,秒杀系统的数据库也须要和业务系统的数据库独立开。

限流削峰

动静分离独立部署能提升系统的响应能力和容量。可是可提供的访问量是必定的,当超过了系统所能承受的容量,该怎么办呢?你可能会说,能够扩容啊。的确是能够,可是扩容也是有限度的。假设单机能承受10万的请求量,预计有1亿的请求量,你要扩容1000台服务器?!这会致使严重的浪费。

首先,上面提到了,秒杀是短时间活动,为了秒杀多部署1000台服务器,秒杀结束后这些服务器再销毁?既浪费硬件资源、又浪费人力资源。

其次,秒杀的商品数量其实并很少,可能秒杀赚的那点钱还不够付服务器和带宽的费用。真·花钱赚吆喝!

咱们该如何处理呢?

上面说了,秒杀的商品数量很少,也就是说,其实最后的真实成交量并不大。再进一步讲,不少的请求都是没用的。

其次,在秒杀前,买家会频繁的刷页面,这又额外增长了无用请求的数量。

咱们只要把这些无用的请求提早都过滤掉,最终到达服务端的请求就会少不少,也就不须要这么多的服务器了。这就是限流削峰。具体作法有不少:

  • 秒杀时间未到时,秒杀按钮置灰:也就是说在秒杀未到时间时,不可发送下单请求。前面咱们已经将页面静态化,分发到了CDN,因此用户的刷新操做只会到CDN。这就削除了刷新操做致使的请求。
  • 秒杀按钮点击后置灰:即避免double-click,一个用户只能点击一次。限制用户点击次数,避免秒杀工具带来过量无效请求。
  • 秒杀前先作题:即在秒杀前须要先作题目,相似验证码功能,实际上是下降了用户的点击频率,也限制了秒杀工具的使用。不过体验很差,不推荐使用。
  • 限制请求次数:能够用js断定,限制用户多少时间间隔内,只能请求多少次。在代理层也能够基于ip作次数限制,限制单ip的请求数量。
  • 直接跳转:假设秒杀已结束或秒杀队列已满,对后续的请求,直接跳转到秒杀结束页面。请求再也不到达服务端。
  • 请求排队:经过消息队列、内存排队等手段,对请求进行排队。相似EDA、Reactor。当队列满了之后,可拒绝后续请求。

服务端优化

上面的「请求排队」,能够作在web服务层,也能够在服务端处理,亦能够两处都处理。除了排队,服务端的优化的核心手段就是缓存,尽可能减小到数据库的数据访问,将热点数据缓存起来。

更极致的优化可能还涉及到:

  • 减小序列化:你们都知道Java序列化和反序列化都是比较耗时的操做,即便使用第三方的序列化工具,也是须要消耗时间的,尽可能减小序列化操做,能减小这部分的时间消耗
  • 不要使用框架:如今通常开发都会使用框架开发,例如SpringMVC。SpringMVC使用了前端控制器,还包括不少的Filter,拦截器等,额外的增长了请求时间。使用纯Servlet,能下降此部分的时间消耗。由于毕竟秒杀逻辑简单,用不用框架,开发效率影响不大。
  • 使用字节流:即便用InputStream、OutputStream,不要使用Writer,Reader。与「减小序列化」相似,编解码也会消耗时间。

另外还有扣库存逻辑处理:

  • 拍下减库存:用户抢到后即扣除库存,可是若是用户抢到了不付款,最后秒杀的商品可能实际并无卖出去。
  • 付款减库存:到用户付款后才去扣库存。这可能致使下单数量远超商品数量。致使的问题是,要么后付款的买家被提示付款失败。要么就是超卖。
  • 预扣库存:用户抢到即扣除库存。规定时间内没有付款则取消订单,恢复库存。这个是经常使用手段

上面说的秒杀系统的通常设计思路。下面来看看咱们是怎么钻漏洞的!

竞赛漏洞

因为竞赛规定,只能使用Java和Spring来处理,对其余队来讲,这就致使了一个比较棘手的问题,IO优化。

Java支持两种IO,BIO和NIO。对于秒杀这种场景来讲,确定不能使用BIO。可是,若是使用Java原生NIO的话,须要处理不少问题,例如半包问题。以最终结果来看,选用原生NIO本身手写异步IO框架的,全都以失败了结。

那就只有另一条路,宁肯扣分,也要使用第三方框架,例如Netty。这就是赌,赌使用JavaNIO的队伍没法完成系统了。

而对咱们队来讲,咱们原先的劣势---前端、一会儿就变成了优点。由于前端有node啊(若是没有node,咱们妥妥的弃赛了)。当时node刚火起来,node天生就是个异步IO框架,竞赛规定里,可没有对前端技术作技术限制!这个漏洞,咱们怎么能不钻?!

公平?公平!

最终竞赛评比时,我发现一个问题,其余队伍很看重公平!!!即先到先得原则,优先到达的请求,优先排队下单。这就致使,在秒杀结束前或请求被处理前,都须要等待,直到服务器处理后才有返回。

这明显增长了服务端的压力,这也是致使他们的吞吐量限制在4000左右的缘由。但不是根本缘由。

根本缘由是这样作就真的公平吗?!这就要看每一个人对公平的理解了!我认为这世上「没有绝对的公平,只有相对的公平」!

你在秒杀系统里排队,保证先到先得,这就是公平吗?

  • 若是一个买家是1M带宽,另外一个买家是100M光纤,他们同时秒杀,你能保证公平吗?
  • 若是你的服务器在北京,北京的买家是否是比广州的买家更容易秒杀到?你能保证公平吗?
  • 若是一个买家是万年死宅,手速奇快;另外一个买家手不太灵活。你能保证公平吗?

既然不能,我为何要在服务端保证公平呢?!

咱们的设计

秒杀就是拼个运气,只要不暗箱操做,那就是公平的。因此咱们不保证先到达的请求就能先买到商品!客户哪知道他是否是先到的呢(虽然这样说,看起来不公平,但实际确实是这样)。因此咱们放弃了所谓的公平

咱们使用了两个队列:

  • 前端node队列
  • 后端下单队列

大体请求流程以下:

  • 假设商品数量为100,那能够设定node队列长度为1000,下单队列长度为100
  • 秒杀开始后,node队列接收前端请求,先到先进。当队列满了之后,直接响应后面的请求,秒杀失败/结束。
  • node队列中的数据批量传递给后端的下单队列,由消费线程从下单队列中获取请求进行处理
  • 若是100个商品所有处理完成(下单后,规定时间内没有付款,取消订单,恢复库存),则秒杀结束
  • 若是100个商品没有处理结束,继续从node队列获取下一批数据处理
  • 若是node队列有空余后,后续的请求继续进入队列
  • node队列中的请求设置超时,规定时间内没有获得处理,直接返回秒杀失败/结束

总结

人员、技术、考量点的不一样都会影响架构设计。一个符合当前人员、技术以及适合考量点的架构,可能能获得意想不到的效果。

相关文章
相关标签/搜索