分布式系统【秒杀系统设计】
一、场景
秒杀活动属于临时高并发写请求,在活动即将开始之前,面临的是高并发的读请求,因为客户会疯狂刷新活动页面了解内容。
二、问题
2.1、超卖
假如库存只有100个,但是最终超卖了200,一般来讲秒杀系统的价格都比较低,如果超卖将严重影响公司的财产利益,因此首当其冲的就是解决商品的超卖问题。今晚超卖损失,由程序员绩效埋单,芜湖。
2.2、高并发
秒杀具有时间短、并发高的特点,秒杀持续时间只有几分钟,而一般公司都为了制造轰动效应,会以极低的价格来吸引用户,因此参与抢购的用户会非常的多。短时间内会有大量请求涌进来,后端如何防止并发过高造成缓存击穿或者失效,击垮数据库都是需要考虑的问题。
2.3、接口防刷
秒杀大多都会出来针对秒杀软件,这类软件会模拟不断向后台服务器发起请求,一秒几百次都是很常见的,如何防止这类软件的重复无效请求,防止不断发起的请求也是需要我们针对性考虑的。
2.4、秒杀URL
对于普通用户来讲,看到的只是一个比较简单的秒杀页面,在未达到规定时间,秒杀按钮是灰色的,一旦到达规定时间,灰色按钮变成可点击状态。这部分是针对小白用户的,如果是稍微有点电脑功底的用户,会通过F12看浏览器的network看到秒杀的url,通过特定软件去请求也可以实现秒杀。或者提前知道秒杀url的人,一请求就直接实现秒杀了。这个问题我们需要考虑解决。
2.5、数据库设计
秒杀有把我们服务器击垮的风险,如果让它与我们的其他业务使用在同一个数据库中,耦合在一起,就很有可能牵连和影响其他的业务。如何防止这类问题发生,就算秒杀发生了宕机、服务器卡死问题,也应该让他尽量不影响线上正常进行的业务。
2.6、大量请求问题
就算使用缓存还是不足以应对短时间的高并发的流量的冲击。如何承载这样巨大的访问量,同时提供稳定低时延的服务保证,是需要面对的一大挑战。我们来算一笔账,假如使用的是redis缓存,单台redis服务器可承受的QPS大概是4W左右,如果一个秒杀吸引的用户量足够多的话,单QPS可能达到几十万,单体redis还是不足以支撑如此巨大的请求量。缓存会被击穿,直接渗透到DB,从而击垮mysql.后台会将会大量报错。
三、设计与技术方案
2.1、数据库设计
4个表:用户表,商品表,订单表,库存表等
2.2、url设计
为了避免URL恶意刷单,需要将秒杀活动Url动态化。具体作法是通过MD5加密随机字符串作为秒杀URl,之后前端访问获取具体url,后台校验通过之后才能继续参与秒杀。
2.3、秒杀页面静态化
将商品的描述、参数、成交记录、图像、评价等全部写入到一个静态页面,用户请求不需要通过访问后端服务器,不需要经过数据库,直接在前台客户端生成,这样可以最大可能的减少服务器的压力。
具体的方法可以使用freemarker模板技术,建立网页模板,填充数据,然后渲染网页。
2.4、单体redis升级为集群redis
秒杀是一个读多写少的场景,使用redis做缓存再合适不过。不过考虑到缓存击穿问题,我们应该构建redis集群,采用哨兵模式,可以提升redis的性能和可用性。
2.5、使用Nginx
nginx是一个高性能web服务器,它的并发能力可以达到几万,而tomcat只有几百。通过nginx映射客户端请求,再分发到后台tomcat服务器集群中可以大大提升并发能力。而且可以把静态资源放到Nginx里,做动静分离。
2.6、精简sql
在扣减库存时,传统做法是先查询库存,再修改库存,需要两个sql。精简后可以用乐观锁中的版本控制,比如:update miaosha_goods set stock =stock-1 where goos_id ={#goods_id} and version = #{version} and sock>0,就可以保证库存不会超卖且一次更新库存,而且乐观锁比悲观锁性能更高。
2.7、Redis预减库存
很多请求进来,都需要后台查询库存,这是一个频繁读的场景。可以使用redis来预减库存,在秒杀开始前可以在redis设值,比如redis.set(goodsId,100),这里预放的库存为100可以设值为常量),每次下单成功后,Integer stock = (Integer)redis.get(goosId); 然后判断sock的值,如果小于常量值就减去1;
不过注意当取消的时候,需要增加库存,增加库存的时候也得注意不能大于之前设定的总库存数(查询库存和扣减库存需要原子操作,此时可以借助lua脚本)下次下单再获取库存的时候,直接从redis里面查就可以了。
2.8、接口限流
秒杀最终的本质是数据库的更新,但是有很多大量无效的请求,我们最终要做的就是如何把这些无效的请求过滤掉,防止渗透到数据库。限流的话,需要入手的方面很多。
1、前端限流
用户在秒杀按钮点击以后发起请求,那么在接下来的5秒是无法点击(通过设置按钮为disable)。这一小举措开发起来成本很小,但是很有效。
2、同一个用户XX秒内重复请求直接拒绝
具体多少秒需要根据实际业务和秒杀的人数而定,一般限定为10秒。具体的做法就是通过redis的键过期策略,首先对每个请求都从String value = redis.get(userId);如果获取到这个value为空或者为null,表示它是有效的请求,然后放行这个请求。如果不为空表示它是重复性请求,直接丢掉这个请求。如果有效,采用redis.setexpire(userId,value,10).value可以是任意值,一般放业务属性比较好,这个是设置以userId为key,10秒的过期时间(10秒后,key对应的值自动为null)。
2.9、算法限流
1、漏桶算法
Leaky Bucket。
2、令牌桶算法
Token Bucket。令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量。
2.10、异步下单
为了提升下单的效率,并且防止下单服务的失败。需要将下单这一操作进行异步处理。最常采用的办法是使用队列,队列最显著的三个优点:异步、削峰、解耦。这里可以采用rabbitmq,在后台经过了限流、库存校验之后,流入到这一步骤的就是有效请求。然后发送到队列里,队列接受消息,异步下单。下完单,入库没有问题可以用短信通知用户秒杀成功。假如失败的话,可以采用补偿机制,重试。
2.11、服务降级
假如在秒杀过程中出现了某个服务器宕机,或者服务不可用,应该做好后备工作。之前的博客里有介绍通过Hystrix进行服务熔断和降级,可以开发一个备用服务,假如服务器真的宕机了,直接给用户一个友好的提示返回,而不是直接卡死,服务器错误等生硬的反馈。
三、采用策略
大概策略有以下几种:缓存策略、
3.1、缓存策略
商品的数据可以放在缓存中,或者将静态资源放到CDN中(CDN加速技术)。减少对数据库的访问压力。
3.2、消息队列【削峰填谷】
用消息队列来存储提交的任务,RPC框架中,将请求写入队列,通过工作线程去处理。
问:为何不用分库分表?
答:分库分表会增加系统复杂性,还要坐数据迁移等。秒杀场景,高并发的写请求并非持续的,可能只有几秒或者几十秒就会结束,如果采用分库分表,得不偿失。
故,可以将秒杀请求暂时写入存储在消息队列中,之后用业务服务器告知用户“秒杀进行中”等,等待释放系统资源后再去处理其他用户请求。
具体就是部署N台队列处理程序,不断的消费消息队列中的任务,然后校验库存,之后下单操作等。用户请求时可以在消息队列中短暂堆积的,当库存为零,消息堆积的请求就可以全部释放。
注意:秒杀过程不能长时间不给用户响应,只能短暂的延迟通知结果。写入流量的大小和数据库处理能力需要提前做好评估,最后根据不同量级决定部署的处理程序台数。
关键字:削峰填谷
3.3、消息队列【异步化机制】
秒杀场景下,一个购买请求流程,是有主次之分的。次要流程不该和主要流程一起同步执行,会增加响应时间。比如主要流程为创建订单和扣减库存,次要流程发放优惠券和增加用户积分等操作就可以放到下单成功后异步处理。
3.4、消息队列【解耦】
比如公司大数据团队有个需求,需要对秒杀活动进行统计数据,用以分析活动商品的受欢迎程度、用户满意度等相关指标。
问:为何不使用传统HTTP或者RPC方式来同步调用?
答:1、整个系统耦合度较高。如果大数据团队接口出问题,会影响我们的秒杀系统。
2、如果大数据团队变更接口,秒杀系统还要响应的更改接口。
消息队列的解耦:
1、秒杀系统产生一条购买数据,我们先将全部数据发送到消息队列中。
2、然后大数据团队自己订阅消息队列Topic。
3、最后他们自己做数据处理方面的工作。
如此,大数据团队的系统故障和接口变更就不会队秒杀系统造成影响,接口变更也不会相互影响,实现了系统解耦。
四、总结
消息对列的削峰填谷、异步处理、解耦在秒杀活动中起到了至关重要的作用。
- 削峰填谷可以削掉到达秒杀系统的峰值流量,让业务逻辑处理更加平滑自然;
- 异步处理可以简化整个业务流程的步骤从而提升系统性能;
- 解耦合可以将秒杀系统和大数据系统解耦开,这样彼此间的任何变更都不会影响到对方,提升系统整体的鲁棒性。