秒杀系统的设计和思考

秒杀系统的难点

首先我们先看下秒杀场景的难点到底在哪?在秒杀场景中最大的问题在于容易产生大并发请求、产生超卖现象和性能问题,下面我们分别分析下下面这三个问题:

1)瞬时大并发: 一提到秒杀系统给人最深刻的印象是超大的瞬时并发,这时你可以联想到小米手机的抢购场景,在小米手机抢购的场景一般都会有10w+的用户同时访问一个商品页面去抢购手机,这就是一个典型的瞬时大并发,如果系统没有经过限流或者熔断处理,那么系统瞬间就会崩掉,就好像被DDos攻击一样;

2)超卖:秒杀除了大并发这样的难点,还有一个所有电商都会遇到的痛,那就是超卖,电商搞大促最怕什么?最怕的就是超卖,产生超卖了以后会影响到用户体验,会导致订单系统、库存系统、供应链等等,产生的问题是一系列的连锁反应,所以电商都不希望超卖发生,但是在大并发的场景最容易发生的就是超卖,不同线程读取到的当前库存数据可能下个毫秒就被其他线程修改了,如果没有一定的锁库存机制那么库存数据必然出错,都不用上万并发,几十并发就可以导致商品超卖;

3)性能: 当遇到大并发和超卖问题后,必然会引出另一个问题,那就是性能问题,如何保证在大并发请求下,系统能够有好的性能,让用户能够有更好的体验,不然每个用户都等几十秒才能知道结果,那体验必然是很糟糕的;


一些方法

1、限流

目的: 限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。

方案: 一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。

常见的限流算法: 令牌桶、漏桶。计数器也可以进行粗暴限流实现。
1):令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:

  • 假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;

  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;

  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;

  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

2)漏桶算法:
漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;

  • 如果桶是空的,则不需流出水滴;

  • 可以以任意速率流入水滴到漏桶;

  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

3) 令牌桶和漏桶对比:

  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;

  • 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;

  • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;

  • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;

  • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;

两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。

4)过载保护: 对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。apiGateway可以设计过载保护策略,设置一段时间允许的连接数。
5)分布式限流:

  • 分布式限流出现的原因:当应用为单点应用时,只要应用进行了限流,那么应用所依赖的各种服务也都得到了保护。但线上业务出于各种原因考虑,多是分布式系统,单节点的限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。而如果实现了分布式限流,那么就可以方便地控制整个服务集群的请求限制,且由于整个集群的请求数量得到了限制,因此服务依赖的各种资源也得到了限流的保护。在这里插入图片描述
    在这里插入图片描述

  • 既然要达到分布式全局限流的效果,那自然需要一个第三方组件来记录请求的次数。其中 Redis 就非常适合这样的场景。每次请求时将方法名进行md5加密后作为Key 写入到 Redis 中,超时时间设置为 2 秒,Redis 将该 Key 的值进行自增。当达到阈值时返回错误。写入 Redis 的操作用 Lua 脚本来完成,利用 Redis 的单线程机制可以保证每个 Redis 请求的原子性。

  • 假设将应用部署到多台机器,应用级限流方式只是单应用内的请求限流,不能进行全局限流。因此我们需要分布式限流和接入层限流来解决这个问题。分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使使用Redis+lua或者nginx+lua技术进行实现,通过这两种技术可以实现的高并发和高性能。首先我们来使用redis+lua实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数。
    Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。Lua本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法。Lua 脚本功能是 Reids在 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。 Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。这点是关键。知道原理了,我们就写一个脚本把判断库存扣减库存的操作都写在一个脚本丢给Redis去做,那到0了后面的都Return False了。

2、流量削峰

目的: 针对于秒杀场景来说,流量往往在一个特定时间点有个高度集中的流量洪峰,这个瞬时对于资源的消耗是很大的,这时往往对于服务的稳定性带来了极大的挑战,如果按照流量洪峰预估系统资源,则可能存在极大的资源浪费。所以协调好处理流量洪峰和资源利用率,最好的方式就是设计错峰方案进行流量削峰。削峰目的:让服务处理请求更加平缓,节省服务器资源。针对于削峰来说,本质上是延缓用户请求的发送,减少和过滤一些无效请求。

方案: 一般采用以下方式:排队、答题、分层过滤
算法:
1)消息队列: 消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,有消息系统来确保信息的可靠专递,消息发布者只管把消息发布到MQ中而不管谁来取,消息使用者只管从MQ中取消息而不管谁发布的,这样发布者和使用者都不用知道对方的存在。典型的使用场景就是将比较耗时而且不需要即时(同步)返回结果的操作,作为消息放入消息队列。

流量削峰首先想到的就是队列,将同步的请求转换成异步请求,将流量峰值通过消息队列平缓推送过去。在高并发分布式环境下,由于来不及同步处理,通过使用消息队列,可以异步处理请求,从而缓解系统的压力。在业务发展初期这些逻辑可能放在一起同步执行,随着业务订单量增长,需要提升系统服务的性能,这时候可以将一些不需要立即生效的操作拆分出来异步执行,比如发短信通知等,这种场景就可以使用消息队列MQ。本质还是通过异步来解决同步的系统压力,所以我们在做架构设计的时候有一个原则:能异步的就尽量不要同步。

2)答题: 一般的电商系统秒杀时会有一个答题流程,主要是为了增加购买的复杂度,首先可以防止一些机器参与秒杀的场景,起到防止作弊的目的。还可以拉大请求时间缓解请求,控制流量达到削峰的目的。这样请求经过一层层的漏斗过滤,会尽量将少的数据请求到后端了。
3)分层过滤: 针对于秒杀场景来说,跟本质的做法是过滤无效请求,分层过滤是采用漏斗方式进行请求处理的。 请求流程:

  • 大部分流量在用户浏览器或者cdn上获取,这一层可以拦截大量数据读取。
  • 前台读系统主要是一些缓存cache,比如采用nginx+lua等方式拦截无效请求。
  • 业务系统主要做好限流,排队等操作。对数据做合理分片。
  • 在最后的数据层做好数据强一致校验,比如保证库存的准确性(不能为负数)。
    在这里插入图片描述

3、服务熔断:

目的: 一般在微服架构中,有一个组件角色叫熔断器。顾名思义,熔断器起的作用就是在特定的场景下关掉当前的通路,从而起到保护整个系统的效果。在微服务架构中,一般我们的独立服务是比较多的,每个独立服务之间划分责任边界,并通过约定协议接口来进行通信。当我们的调用链路复杂依赖多时,很可能会发生雪崩效应。当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。

4、服务降级:

目的: 服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。
与熔断区别: 触发原因不同,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;


实践方案:

从整个秒杀系统的架构其实和一般的互联网系统架构本身没有太多的不同,核心理念还是通过缓存、异步、限流来保证系统的高并发和高可用。下面从一笔秒杀交易的流程来描述下秒杀系统架构设计的要点:
高并发的处理:
1)对于大促时候的秒杀活动,一般运营会配置静态的活动页面,配置静态活动页面主要有两个目的一方面是为了便于在各种社交媒体分发,另一方面是因为秒杀活动页的流量是大促期间最大的,通过配置成静态页面可以将页面发布在公有云上动态的横向扩展;

2)将秒杀活动的静态页面提前刷新到CDN节点,通过CDN节点的页面缓存来缓解访问压力和公司网络带宽,CDN上缓存js、css和图片;

3)将活动H5页面部署在公有云的web server上,使用公有云最大的好处就是能够根据活动的火爆程度动态扩容而且成本较低,同时将访问压力隔离在公司系统外部;

4)在提供真正商品秒杀业务功能的app server上,需要进行交易限流、熔断控制,防止因为秒杀交易影响到其他正常服务的提供,我们在限流和熔断方面使用了hystrix,在核心交易的controller层通过hystrix进行交易并发限流控制,当交易流量超出我们设定的限流最大值时,会对新交易进行熔断处理固定返回静态失败报文。

5)服务降级处理,除了上面讲到的限流和熔断控制,我们还设定了降级开关,对于首页、购物车、订单查询、大数据等功能都会进行一定程度的服务降级,例如我们会对首页原先动态生成的大数据页面布局降级为所有人看到的是一样的页面、购物车也会降级为不在一级页面的tabbar上的购物车图标上显示商品数量、历史订单的查询也会提供时间周期较短的查询、大数据商品推荐也会提供一样的商品推荐,通过这样的降级处理能够很好的保证各个系统在大促期间能够正常的提供最基本的服务,保证用户能够正常下单完成付款。

超卖的处理:
我们日常的下单过程中防止超卖一般是通过在数据库上实施乐观锁来完成,使用乐观锁虽然比for update这种悲观锁方式性能要好很多,但是还是无法满足秒杀的上万并发需求,通过实时库存的扣减在缓存中进行,异步扣减数据库中的库存,保证缓存中和数据库中库存的最终一致性。秒杀本来就是读多写少,使用Redis集群,主从同步、读写分离或哨兵机制。

在这个方案中我们使用的分布式缓存是redis,使用了codis集群方案稳定性和高可用方面还是比较有保证的,因为redis是单线程写,所以也不用担心线程安全的问题,redis自身就能够保证数据的强一致性,在下单的事务中包含了实时扣减缓存中的库存和异步发送队列,由队列处理器再异步从队列中取出订单根据订单信息扣减库存系统数据库中的商品数量。

性能的处理:
1):水平扩容和nginx 负载均衡结合。
2):库存预热: 开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中去,让整个流程都在Redis里面去做,然后等秒杀介绍了,再异步的去修改库存就好了。

猜你喜欢

转载自blog.csdn.net/u014618114/article/details/107968187