天天看點

商城秒殺活動要點

商城秒殺的特性:

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();
          }
      }
    }