秒杀系统设计思路

通用业务问题

1.1 秒杀系统介绍

    参考博客:https://blog.csdn.net/bigtree_3721/article/details/72760538

  1、秒杀业务分析

    1)正常电子商务流程

        (1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货

    2)秒杀业务的特性

        (1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)一般是定时上架;(5)时间短、瞬时并发量高;

  2、秒杀技术特点 & 解决方法

    1)读多写少

         缓存

    2)高并发

         限流、负载均衡、缓存、异步并发(消息队列中间件)、队列

    3)资源冲突(超卖)

        数据库锁(乐观锁,悲观锁)、 分布式锁(redis、zk)

  3、各层级常用解决方法

      

1.2 秒杀系统技术挑战

  1、对现有网站业务造成冲击

      1. 解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离。

  2、高并发下的应用、数据库负载

      1. 用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构

      2. 访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。

      3. 解决方案:重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,用户请求不需要经过应用服务。

  3、突然增加的网络及服务器带宽

      1. 解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,

                        需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽。

  4、直接下单

      1. 秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。

      2. 而下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。

      解决方案:为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。

                 办法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到。

  5、如何控制秒杀商品页面购买按钮的点亮

    1)问题描述

        1. 购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的。

        2. 为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计为静态页面

        3. 缓存在CDN、反向代理服务器上,甚至用户浏览器上,秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。

    2)解决方案

        1. 使用JavaScript脚本控制,在秒杀商品静态页面中加入一个JavaScript文件引用,该JavaScript文件中包含 秒杀开始标志为否;

        2. 当秒杀开始的时候生成一个新的JavaScript文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是

        3. 加入下单页面的URL及随机数参数

          这个随机数只会产生一个,这个JavaScript文件非常小,即使每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽造成太大压力。

          即所有人看到的URL都是同一个,服务器端可以用redis这种分布式缓存服务器来保存随机数),并被用户浏览器加载,控制秒杀商品页面的展示。

          这个JavaScript文件的加载可以加上随机版本号(例如xx.js?v=32353823),这样就不会被浏览器、CDN和反向代理服务器缓存。

  6、如何只允许第一个提交的订单被发送到订单子系统

    1)问题描述      

        1. 由于最终能够成功秒杀到商品的用户只有一个,因此需要在用户提交订单时,检查是否已经有订单提交。

        2. 如果已经有订单提交成功,则需要更新 JavaScript文件,更新秒杀开始标志为否,购买按钮变灰。

        3. 事实上,由于最终能够成功提交订单的用户只有一个,为了减轻下单页面服务器的负载压力,
            可以控制进入下单页面的入口,只有少数用户能进入下单页面,其他用户直接进入秒杀结束页面。

     2)解决方案

        1. 假设下单服务器集群有10台服务器,每台服务器只接受最多10个下单请求。

        2. 在还没有人提交订单成功之前,如果一台服务器已经有十单了,而有的一单都没处理,
            可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面,再刷新一下页面,
            有可能被一单都没有处理的服务器处理,进入了填写订单的页面,可以考虑通过cookie的方式来应对,符合一致性原则。
            当然可以采用最少连接的负载均衡算法,出现上述情况的概率大大降低。

  7、如何进行下单前置检查

    1)下单服务器检查本机已处理的下单请求数目

        1. 如果超过10条,直接返回已结束页面给用户;

        2. 如果未超过10条,则用户可进入填写订单及确认页面;

    2)检查全局已提交订单数目

        1. 已超过秒杀商品总数,返回已结束页面给用户;

        2. 未超过秒杀商品总数,提交到子订单系统;

    PS:

        1. 由于秒杀商品数量通常极少,可以直接在集群的每台下单服务器上都设置一个最大下单数来限制本服务器上提交的请求数

        2. 超过该高数目后,其他请求直接跳转至秒杀活动结束页面。这样可以在在全局判断之前在每台服务器上完成初步过滤。

  8、秒杀一般是定时上架

      1. 提前设定好商品的上架时间,用户可以在前台看到该商品,但是无法点击“立即购买”的按钮。

      2. 但是需要考虑的是,有人可以绕过前端的限制,直接通过URL的方式发起购买,这就需要在前台商品页面,以及bug页面到后端的数据库,都要进行时钟同步。

  9、减库存的操作

      1. 有两种选择,一种是拍下减库存 另外一种是付款减库存;目前采用的“拍下减库存”的方式,拍下就是一瞬间的事,对用户体验会好些。

  10、“超卖”问题

      1. 由于库存并发更新的问题,导致在实际库存已经不足的情况下,库存依然在减,导致卖家的商品卖得件数超过秒杀的预期。

      方案:采用乐观锁

  11、秒杀器的应对

      1. 秒杀器一般下单个购买及其迅速,根据购买记录可以甄别出一部分,可以通过校验码达到一定的方法,这就要求校验码足够安全,不被破解

      2. 采用的方式有:秒杀专用验证码,电视公布验证码,秒杀答题。

1.3 秒杀架构原则

  1、尽量将请求拦截在系统上游

      1. 传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时

      2. 流量虽大,下单成功的有效流量甚小【一趟火车其实只有2000张票,200w个人来买,基本没有人能买成功,请求有效率为0】。

  2、读多写少的常用多使用缓存

      1. 一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%,非常适合使用缓存。

1.4 前端层设计

  1、需要解决的几个问题

     1)第一个是秒杀页面的展示

        1. 我们知道一个html页面还是比较大的,即使做了压缩,http头和内容的大小也可能高达数十K,加上其他的css, js,图片等资源

        2. 如果同时有几千万人参与一个商品的抢购,一般机房带宽也就只有1G~10G,网络带宽就极有可能成为瓶颈

        3. 所以这个页面上各类静态资源首先应分开存放,然后放到cdn节点上分散压力,由于CDN节点遍布全国各地,能缓冲掉绝大部分的压力

    2)第二个是倒计时

        1. 出于性能原因这个一般由js调用客户端本地时间,就有可能出现客户端时钟与服务器时钟不一致,另外服务器之间也是有可能出现时钟不一致。

        2. 客户端与服务器时钟不一致可以采用客户端定时和服务器同步时间,这里考虑一下性能问题。

        3. web服务器之间时间不同步可以采用统一时间服务器的方式,比如每隔1分钟所有参与秒杀活动的web服务器就与时间服务器做一次时间同步。

    3)浏览器层请求拦截

        1. 产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求;

        2. JS层面,限制用户在x秒之内只能提交一次请求;

  2、站点层设计

      注:前端层的请求拦截,只能拦住小白用户,直接调用你后端的http请求,怎么整?

      1. 同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面

      2. 同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面

      3. 如此限流,又有99%的流量会被拦截在站点层。

  3、服务层设计

    1)处理写请求

        1. 服务层,清楚的知道小米只有1万部手机,我透10w个请求去数据库没有任何意义。

        2. 可以做请求队列,每次只透过有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”;

    2)对于读请求

        1. cache来抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的;

        2. 用户请求分发模块:使用Nginx或Apache将用户的请求分发到不同的机器上。

        3. 用户请求预处理模块:判断商品是不是还有剩余来决定是不是要处理该请求。

        4. 用户请求处理模块:把通过预处理的请求封装成事务提交给数据库,并返回是否成功。

        5. 数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。

1.5 数据库设计

  1、数据库设计要考虑的问题

      1. 如何保证数据可用性;

      2. 如何提高数据库读性能(大部分应用读多写少,读会先成为瓶颈);

      3. 如何保证一致性;

      4. 如何提高扩展性;

  2、如何保证数据的可用性?(问题1)

      1. 解决可用性问题的思路是=>冗余

      2. 如何保证站点的可用性?复制站点,冗余站点

      3. 如何保证服务的可用性?复制服务,冗余服务

      4. 如何保证数据的可用性?复制数据,冗余数据

      注:数据的冗余,会带来一个副作用=>引发一致性问题(先不说一致性问题,先说可用性)。

  3、如何保证数据库“读”高可用?(问题2)

      1. 实际的玩法:服务+数据库+缓存 的方式提供数据访问,用cache提高读性能。

      2. 业务层不直接面向db和cache,服务层屏蔽了底层db、cache的复杂性。

      注:不管采用主从的方式扩展读性能,还是缓存的方式扩展读性能,数据都要复制多份(主+从,db+cache),一定会引发一致性问题。

          

    主从数据库的一致性,通常有两种解决方案

    1)中间件

        如果某一个key有写操作,在不一致时间窗口内,中间件会将这个key的读操作也路由到主库上。

        这个方案的缺点是,数据库中间件的门槛较高(百度,腾讯,阿里,360等一些公司有)。

    2)强制读主

         实际用的“双主当主从用”的架构,不存在主从不一致的问题。

    第二类不一致,是db与缓存间的不一致

     1)常见缓存架构写顺序

        淘汰cache     == >     写数据库;

    2)读操作的顺序

        读cache,如果cache hit则返回;

        如果cache miss,则读从库;

        读从库后,将数据放回cache;

        问题:从库读到旧数据(同步还没有完成),旧数据入cache

          在经验“主从同步延时窗口时间”后,再次发起一个异步淘汰cache的请求;带来的代价是,多引入一次读miss(成本可以忽略)。

          除此之外,最佳实践之一是:建议为所有cache中的item设置一个超时时间。

1.6 解决大并发问题 绍

  1、请求接口的合理设计

      1. 一个秒杀或者抢购页面,通常分为2个部分,一个是静态的HTML等内容,另一个就是参与秒杀的Web后台请求接口。

      2. 通常静态HTML等内容,是通过CDN的部署,一般压力不大,核心瓶颈实际上在后台请求接口上。

      3. 这个后端接口,必须能够支持高并发请求,同时,非常重要的一点,必须尽可能“快”,在最短的时间里返回用户的请求结果。

      4. 为了实现尽可能快这一点,接口的后端存储使用内存级别的操作会更好一点。

      

  2、重启与过载保护

      1. 秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。

      2. 如果检测到系统满负载状态,拒绝请求也是一种保护措施。

1.7 作弊的手段:进攻与防守

  1、同一个账号,一次性发出多个请求

    1)问题描述

        1. 例如一个简单的领取逻辑,先判断用户是否有参与记录,如果没有则领取成功,最后写入到参与记录中。

        2. 然而,在某个请求成功写入参与记录的时间差内,其他的请求获查询到的结果都是“没有参与记录”。

    2)应对方案

        1. 在程序入口处,一个账号只允许接受1个请求,其他请求过滤,不仅解决了同一个账号,发送N个请求的问题,还保证了后续的逻辑流程的安全。

        2. 实现方案,可以通过Redis这种内存缓存服务,写入一个标志位(只允许1个请求写成功,结合watch的乐观锁的特性),成功写入的则可以继续参加。

  2、 多个账号,一次性发送多个请求 

    1)问题描述    

        1. 举个例子,例如微博中有转发抽奖的活动,如果我们使用几万个“僵尸号”去混进去转发,这样就可以大大提升我们中奖的概率。

    2)应对方案

        1. 可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很高,可以给它弹出一个验证码或者直接禁止它的请求

  3、多个账号,不同IP发送不同请求

1.8 高并发下的数据安全

  1、悲观锁思路 

      1. 悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

      2. 然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。

      3. 会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。

      4. 同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

  2、FIFO队列思路

      1. 我们直接将请求放入队列中的,采用FIFO(先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。

      2. 我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”。

  3、乐观锁思路

      1. 乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。

      2. 实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。

      3. 有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

1.9 真实案例讲解

  1、模块

      1. 项目分成供应链和商城两大部分,供应链主要功能包括库存控制、商品信息录入、订单发货等;

      2. 商城主要功能包括:商品搜索、秒杀、购买、支付、评论等;

      3. 负责实现了购物车、支付、优惠券、秒杀等功能设计实现

  2、主要技术点

      1. 技术实现采用了RESTful设计,前后端分离;

      2. 后端整体基于Python/Django + MySQL 开发,

      3. 采用了 Redis 缓存,Celery 异步任务,ElasticSearch搜索;

      4. 使用Docker部署交付;

  3、秒杀系统设计

      1.秒杀服务器部署与主站分离,防止整体宕机。

      2.秒杀商品提前存入Redis List,size为秒杀数量。

      3.在Nginx层 Web 层分别做限流,活动开始后,将用户购买请求放入RabbitMQ中排队,
        消费者依次生成订单,同时减少Redis List中商品数量,并将生成的订单插入Redis和MySQL中供用户查询购买结果。

      4.当商品数量为0时,剩余消息处理为抢购失败。
        通过模拟测试,上述架构可支撑10w人抢购2000件商品的情景,并在60秒内处理完全部订单,
        配置如下:在 aws 上,共启用6台 ec2 实例 ( Nginx + Worker x 4 + RabbitMQ + Redis cluster)

原文地址:https://www.cnblogs.com/jiaxinzhu/p/11782219.html