1. 程式人生 > >秒殺系統架構分析與實戰

秒殺系統架構分析與實戰

選擇 return 暫時 memcached something 關心 附加 ron sta

1 秒殺業務分析

  • 正常電子商務流程(1)查詢商品;(2)創建訂單;(3)扣減庫存;(4)更新訂單;(5)付款;(6)賣家發貨

  • 秒殺業務的特性(1)低廉價格;(2)大幅推廣;(3)瞬時售空;(4)一般是定時上架;(5)時間短、瞬時並發量高;

2 秒殺技術挑戰

假設某網站秒殺活動只推出一件商品,預計會吸引1萬人參加活動,也就說最大並發請求數是10000,秒殺系統需要面對的技術挑戰有:

  1. 對現有網站業務造成沖擊秒殺活動只是網站營銷的一個附加活動,這個活動具有時間短,並發訪問量大的特點,如果和網站原有應用部署在一起,必然會對現有業務造成沖擊,稍有不慎可能導致整個網站癱瘓。解決方案:將秒殺系統獨立部署,甚至使用獨立域名,使其與網站完全隔離

  2. 高並發下的應用、數據庫負載用戶在秒殺開始前,通過不停刷新瀏覽器頁面以保證不會錯過秒殺,這些請求如果按照一般的網站應用架構,訪問應用服務器、連接數據庫,會對應用服務器和數據庫服務器造成負載壓力。解決方案:重新設計秒殺商品頁面,不使用網站原來的商品詳細頁面,頁面內容靜態化,用戶請求不需要經過應用服務

  3. 突然增加的網絡及服務器帶寬假設商品頁面大小200K(主要是商品圖片大小),那麽需要的網絡和服務器帶寬是2G(200K×10000),這些網絡帶寬是因為秒殺活動新增的,超過網站平時使用的帶寬。解決方案:因為秒殺新增的網絡帶寬,必須和運營商重新購買或者租借。為了減輕網站服務器的壓力,需要將秒殺商品頁面緩存在CDN,同樣需要和CDN服務商臨時租借新增的出口帶寬

  4. 直接下單秒殺的遊戲規則是到了秒殺才能開始對商品下單購買,在此時間點之前,只能瀏覽商品信息,不能下單。而下單頁面也是一個普通的URL,如果得到這個URL,不用等到秒殺開始就可以下單了。解決方案:為了避免用戶直接訪問下單頁面URL,需要將改URL動態化,即使秒殺系統的開發者也無法在秒殺開始前訪問下單頁面的URL。辦法是在下單頁面URL加入由服務器端生成的隨機數作為參數,在秒殺開始的時候才能得到

  5. 如何控制秒殺商品頁面購買按鈕的點亮購買按鈕只有在秒殺開始的時候才能點亮,在此之前是灰色的。如果該頁面是 動態生成的,當然可以在服務器端構造響應頁面輸出,控制該按鈕是灰色還 是點亮,但是為了減輕服務器端負載壓力,更好地利用CDN、反向代理等性能優化手段,該頁面被設計為靜態頁面,緩存在CDN、反向代理服務器上,甚至用戶 瀏覽器上。秒殺開始時,用戶刷新頁面,請求根本不會到達應用服務器。解決方案

    :使用JavaScript腳本控制,在秒殺商品靜態頁面中加入一個JavaScript文件引用,該JavaScript文件中包含 秒殺開始標誌為否;當秒殺開始的時候生成一個新的JavaScript文件(文件名保持不變,只是內容不一樣),更新秒殺開始標誌為是,加入下單頁面的URL及隨機數參數(這個隨機數只會產生一個,即所有人看到的URL都是同一個,服務器端可以用redis這種分布式緩存服務器來保存隨機數),並被用戶瀏覽器加載,控制秒殺商品頁面的展示。這個JavaScript文件的加載可以加上隨機版本號(例如xx.js?v=32353823),這樣就不會被瀏覽器、CDN和反向代理服務器緩存。這個JavaScript文件非常小,即使每次瀏覽器刷新都訪問JavaScript文件服務器也不會對服務器集群和網絡帶寬造成太大壓力。

  6. 如何只允許第一個提交的訂單被發送到訂單子系統由於最終能夠成功秒殺到商品的用戶只有一個,因此需要在用戶提 交訂單時,檢查是否已經有訂單提交。如果已經有訂單提交成功,則需要更新 JavaScript文件,更新秒殺開始標誌為否,購買按鈕變灰。事實上,由於最終能夠成功提交訂單的用戶只有一個,為了減輕下單頁面服務器的負載壓力, 可以控制進入下單頁面的入口,只有少數用戶能進入下單頁面,其他用戶直接進入秒殺結束頁面。解決方案:假設下單服務 器集群有10臺服務器,每臺服務器只接受最多10個下單請求。在還沒有人提交訂單成功之前,如果一臺服務器已經有十單了,而有的一單都沒處理,可能出現的 用戶體驗不佳的場景是用戶第一次點擊購買按鈕進入已結束頁面,再刷新一下頁面,有可能被一單都沒有處理的服務器處理,進入了填寫訂單的頁面,可以考慮通過cookie的方式來應對,符合一致性原則。當然可以采用最少連接的負載均衡算法,出現上述情況的概率大大降低。

  7. 如何進行下單前置檢查

    • 下單服務器檢查本機已處理的下單請求數目:

    如果超過10條,直接返回已結束頁面給用戶;

    如果未超過10條,則用戶可進入填寫訂單及確認頁面;

    • 檢查全局已提交訂單數目:

    已超過秒殺商品總數,返回已結束頁面給用戶;

    未超過秒殺商品總數,提交到子訂單系統;

  8. 秒殺一般是定時上架該功能實現方式很多。不過目前比較好的方式是:提前設定好商品的上架時間,用戶可以在前臺看到該商品,但是無法點擊“立即購買”的按鈕。但是需要考慮的是,有人可以繞過前端的限制,直接通過URL的方式發起購買,這就需要在前臺商品頁面,以及bug頁面到後端的數據庫,都要進行時鐘同步。越在後端控制,安全性越高。定時秒殺的話,就要避免賣家在秒殺前對商品做編輯帶來的不可預期的影響。這種特殊的變更需要多方面評估。一般禁止編輯,如需變更,可以走數據訂正多的流程。

  9. 減庫存的操作有兩種選擇,一種是拍下減庫存 另外一種是付款減庫存;目前采用的“拍下減庫存”的方式,拍下就是一瞬間的事,對用戶體驗會好些。

  10. 庫存會帶來“超賣”的問題:售出數量多於庫存數量由於庫存並發更新的問題,導致在實際庫存已經不足的情況下,庫存依然在減,導致賣家的商品賣得件數超過秒殺的預期。方案:采用樂觀鎖

    update auction_auctions set
    quantity = #inQuantity#
    where auction_id = #itemId# and quantity = #dbQuantity#

  11. 秒殺器的應對秒殺器一般下單個購買及其迅速,根據購買記錄可以甄別出一部分。可以通過校驗碼達到一定的方法,這就要求校驗碼足夠安全,不被破解,采用的方式有:秒殺專用驗證碼,電視公布驗證碼,秒殺答題

3 秒殺架構原則

  1. 盡量將請求攔截在系統上遊傳統秒殺系統之所以掛,請求都壓倒了後端數據層,數據讀寫鎖沖突嚴重,並發高響應慢,幾乎所有請求都超時,流量雖大,下單成功的有效流量甚小【一趟火車其實只有2000張票,200w個人來買,基本沒有人能買成功,請求有效率為0】。

  2. 讀多寫少的常用多使用緩存這是一個典型的讀多寫少的應用場景【一趟火車其實只有2000張票,200w個人來買,最多2000個人下單成功,其他人都是查詢庫存,寫比例只有0.1%,讀比例占99.9%】,非常適合使用緩存

4 秒殺架構設計

秒殺系統為秒殺而設計,不同於一般的網購行為,參與秒殺活動的用戶更關心的是如何能快速刷新商品頁面,在秒殺開始的時候搶先進入下單頁面,而不是商品詳情等用戶體驗細節,因此秒殺系統的頁面設計應盡可能簡單。

商品頁面中的購買按鈕只有在秒殺活動開始的時候才變亮,在此之前及秒殺商品賣出後,該按鈕都是灰色的,不可以點擊。

下單表單也盡可能簡單,購買數量只能是一個且不可以修改,送貨地址和付款方式都使用用戶默認設置,沒有默認也可以不填,允許等訂單提交後修改;只有第一個提交的訂單發送給網站的訂單子系統,其余用戶提交訂單後只能看到秒殺結束頁面。

要做一個這樣的秒殺系統,業務會分為兩個階段,第一個階段是秒殺開始前某個時間到秒殺開始, 這個階段可以稱之為準備階段,用戶在準備階段等待秒殺; 第二個階段就是秒殺開始到所有參與秒殺的用戶獲得秒殺結果, 這個就稱為秒殺階段吧。

4.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秒之內只能提交一次請求;

4.2 站點層設計

前端層的請求攔截,只能攔住小白用戶(不過這是99%的用戶喲),高端的程序員根本不吃這一套,寫個for循環,直接調用你後端的http請求,怎麽整?

(1)同一個uid,限制訪問頻度,做頁面緩存,x秒內到達站點層的請求,均返回同一頁面

(2)同一個item的查詢,例如手機車次,做頁面緩存,x秒內到達站點層的請求,均返回同一頁面

如此限流,又有99%的流量會被攔截在站點層。

4.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(); 
            } 
        } 
      } 
      4.4 數據庫設計

秒殺系統架構分析與實戰