商城秒杀活动要点

商城秒杀的特性:

1、定时秒杀。即商品在秒杀时间点之前是不能进行购买下单。业务较简单。

2、秒杀前用户会频繁刷新秒杀页面。

3、秒杀持续时间短、瞬时访问流量高。

4、同一用户/IP禁止秒杀多次。

秒杀系统设计要点:

1、将秒杀系统独立部署,甚至使用独立域名,使其与原有网站完全隔离。主要防止秒杀对现有网站业务造成冲击。

2、页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。

3、禁止重复提交:用户提交之后按钮置灰,禁止重复提交 用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流

4、秒杀提交地址在秒杀开始的时候才能得到。

5、使用缓存、队列。将秒杀数量提前写入redis中。接收到请求时,直接递减redis中总数量,如果下单数量为0则请求直接返回。下单过程失败则增加redis中总数量。

秒杀架构设计

秒杀系统为秒杀而设计,不同于一般的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,而不是商品详情等用户体验细节,因此秒杀系统的页面设计应尽可能简单。

商品页面中的购买按钮只有在秒杀活动开始的时候才变亮,在此之前及秒杀商品卖出后,该按钮都是灰色的,不可以点击。

下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许等订单提交后修改;只有第一个提交的订单发送给网站的订单子系统,其余用户提交订单后只能看到秒杀结束页面。

要做一个这样的秒杀系统,业务会分为两个阶段,第一个阶段是秒杀开始前某个时间到秒杀开始, 这个阶段可以称之为准备阶段,用户在准备阶段等待秒杀; 第二个阶段就是秒杀开始到所有参与秒杀的用户获得秒杀结果, 这个就称为秒杀阶段吧。

1 前端层设计

首先要有一个展示秒杀商品的页面, 在这个页面上做一个秒杀活动开始的倒计时, 在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面。这里需要考虑两个问题:

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

    我们知道一个html页面还是比较大的,即使做了压缩,http头和内容的大小也可能高达数十K,加上其他的css, js,图片等资源,如果同时有几千万人参与一个商品的抢购,一般机房带宽也就只有1G~10G,网络带宽就极有可能成为瓶颈,所以这个页面上各类静态资源首先应分开存放,然后放到cdn节点上分散压力,由于CDN节点遍布全国各地,能缓冲掉绝大部分的压力,而且还比机房带宽便宜~

  2. 第二个是倒计时

    出于性能原因这个一般由js调用客户端本地时间,就有可能出现客户端时钟与服务器时钟不一致,另外服务器之间也是有可能出现时钟不一致。客户端与服务器时钟不一致可以采用客户端定时和服务器同步时间,这里考虑一下性能问题,用于同步时间的接口由于不涉及到后端逻辑,只需要将当前web服务器的时间发送给客户端就可以了,因此速度很快,就我以前测试的结果来看,一台标准的web服务器2W+QPS不会有问题,如果100W人同时刷,100W QPS也只需要50台web,一台硬件LB就可以了~,并且web服务器群是可以很容易的横向扩展的(LB+DNS轮询),这个接口可以只返回一小段json格式的数据,而且可以优化一下减少不必要cookie和其他http头的信息,所以数据量不会很大,一般来说网络不会成为瓶颈,即使成为瓶颈也可以考虑多机房专线连通,加智能DNS的解决方案;web服务器之间时间不同步可以采用统一时间服务器的方式,比如每隔1分钟所有参与秒杀活动的web服务器就与时间服务器做一次时间同步

  3. 浏览器层请求拦截

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

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

2 站点层设计

前端层的请求拦截,只能拦住小白用户(不过这是99%的用户哟),高端的程序员根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?

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

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

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

3 服务层设计

站点层的请求拦截,只能拦住普通程序员,高级黑客,假设他控制了10w台肉鸡(并且假设买票不需要实名认证),这下uid的限制不行了吧?怎么整?

(1)大哥,我是服务层,我清楚的知道小米只有1万部手机,我清楚的知道一列火车只有2000张车票,我透10w个请求去数据库有什么意义呢?对于写请求,做请求队列,每次只透过有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”

(2)对于读请求,还用说么?cache来抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的;

如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。

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

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

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

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

  • 用户请求预处理模块

    经过HTTP服务器的分发后,单个服务器的负载相对低了一些,但总量依然可能很大,如果后台商品已经被秒杀完毕,那么直接给后来的请求返回秒杀失败即可,不必再进一步发送事务了,示例代码可以如下所示:

    package seckill;
    import org.apache.http.HttpRequest;
    /**
    * 预处理阶段,把不必要的请求直接驳回,必要的请求添加到队列中进入下一阶段.
    */
    public class PreProcessor {
      // 商品是否还有剩余
      private static boolean reminds = true;
      private static void forbidden() {
          // Do something.
      }
      public static boolean checkReminds() {
          if (reminds) {
              // 远程检测是否还有剩余,该RPC接口应由数据库服务器提供,不必完全严格检查.
              if (!RPC.checkReminds()) {
                  reminds = false;
              }
          }
          return reminds;
      }
      /**
       * 每一个HTTP请求都要经过该预处理.
       */
      public static void preProcess(HttpRequest request) {
          if (checkReminds()) {
              // 一个并发的队列
              RequestQueue.queue.add(request);
          } else {
              // 如果已经没有商品了,则直接驳回请求即可.
              forbidden();
          }
      }
    }
    
    • 并发队列的选择

    Java的并发包提供了三个常用的并发队列实现,分别是:ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue。

    ArrayBlockingQueue是初始容量固定的阻塞队列,我们可以用来作为数据库模块成功竞拍的队列,比如有10个商品,那么我们就设定一个10大小的数组队列。

    ConcurrentLinkedQueue使用的是CAS原语无锁队列实现,是一个异步队列,入队的速度很快,出队进行了加锁,性能稍慢。

    LinkedBlockingQueue也是阻塞的队列,入队和出队都用了加锁,当队空的时候线程会暂时阻塞。

    由于我们的系统入队需求要远大于出队需求,一般不会出现队空的情况,所以我们可以选择ConcurrentLinkedQueue来作为我们的请求队列实现:

    package seckill;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.ConcurrentLinkedQueue;
    import org.apache.http.HttpRequest;
    public class RequestQueue {
      public static ConcurrentLinkedQueue<HttpRequest> queue = new ConcurrentLinkedQueue<HttpRequest>();
    }
    
  • 用户请求模块

    package seckill;
    import org.apache.http.HttpRequest;
    public class Processor {
      /**
       * 发送秒杀事务到数据库队列.
       */
      public static void kill(BidInfo info) {
          DB.bids.add(info);
      }
      public static void process() {
          BidInfo info = new BidInfo(RequestQueue.queue.poll());
          if (info != null) {
              kill(info);
          }
      }
    }
    class BidInfo {
      BidInfo(HttpRequest request) {
          // Do something.
      }
    }
    
  • 数据库模块

    数据库主要是使用一个ArrayBlockingQueue来暂存有可能成功的用户请求。

    package seckill;
    import java.util.concurrent.ArrayBlockingQueue;
    /**
    * DB应该是数据库的唯一接口.
    */
    public class DB {
      public static int count = 10;
      public static ArrayBlockingQueue<BidInfo> bids = new ArrayBlockingQueue<BidInfo>(10);
      public static boolean checkReminds() {
          // TODO
          return true;
      }
      // 单线程操作
      public static void bid() {
          BidInfo info = bids.poll();
          while (count-- > 0) {
              // insert into table Bids values(item_id, user_id, bid_date, other)
              // select count(id) from Bids where item_id = ?
              // 如果数据库商品数量大约总数,则标志秒杀已完成,设置标志位reminds = false.
              info = bids.poll();
          }
      }
    }
posted @ 2018-06-20 17:59  沈建军  阅读(596)  评论(0编辑  收藏  举报