如何设计一个高并发的秒杀系统

2019-06-22 09:06:22

秒杀系统一般设计思路

秒杀系统的特点是:

§ 瞬时请求量很高

§ 持续时间较短

所以秒杀系统需要解决的是「在高并发情况下,用户请求及数据更新的问题」!

一般的设计思路:

§ (变相)扩容

§ 提高性能

具体方式有:

动静分离

对于一般的应用来说,请求流程大致如下:

§ 服务端接收到请求,从数据库中查询相应数据

§ 选择对应的展示模板

§ 通过模板和数据渲染出最终页面

§ 将页面返回给客户端

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

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

模板是个静态的内容,部署后一般是不会变化的;而数据是个相对动态的内容,根据请求参数的不同,数据可能不同。所以我们需要将模板与数据分离。

以前的做法是后端事先生成渲染后的页面,缓存起来或直接部署到静态服务器或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。与「减少序列化」类似,编解码也会消耗时间。

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

§ 拍下减库存:用户抢到后即扣除库存,但是如果用户抢到了不付款,最后秒杀的商品可能实际并没有卖出去。

§ 付款减库存:到用户付款后才去扣库存。这可能导致下单数量远超商品数量。导致的问题是,要么后付款的买家被提示付款失败。要么就是超卖。

§ 预扣库存:用户抢到即扣除库存。规定时间内没有付款则取消订单,恢复库存。这个是常用手段

上面说的秒杀系统的一般设计思路。然后我们就要来考虑秒杀系统的公平性

公平?公平!

习惯性的思维告诉我们先到先得原则,优先到达的请求,优先排队下单。这就导致,在秒杀结束前或请求被处理前,都需要等待,直到服务器处理后才有返回。

这明显增加了服务端的压力,这也是导致的吞吐性能被严重影响到。但不是根本原因。

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

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

§ 如果一个买家是1M带宽,另一个买家是100M光纤,他们同时秒杀,你能保证公平吗?

§ 如果你的服务器在北京,北京的买家是不是比广州的买家更容易秒杀到?你能保证公平吗?

§ 如果一个买家是万年死宅,手速奇快;另一个买家手不太灵活。你能保证公平吗?

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

想法

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

我使用了两个队列:

§ 前端node队列

§ 后端下单队列

大致请求流程如下:

§ 假设商品数量为100,那可以设定node队列长度为1000,下单队列长度为100

§ 秒杀开始后,node队列接收前端请求,先到先进。当队列满了以后,直接响应后面的请求,秒杀失败/结束。

§ node队列中的数据批量传递给后端的下单队列,由消费线程从下单队列中获取请求进行处理

§ 如果100个商品全部处理完成(下单后,规定时间内没有付款,取消订单,恢复库存),则秒杀结束

§ 如果100个商品没有处理结束,继续从node队列获取下一批数据处理

§ 如果node队列有空余后,后续的请求继续进入队列

§ node队列中的请求设置超时,规定时间内没有得到处理,直接返回秒杀失败/结束

总结

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

 


相关资讯
返回