商城秒殺的特性:
1、定時秒殺。即商品在秒殺時間點之前是不能進行購買下單。業務較簡單。
2、秒殺前使用者會頻繁重新整理秒殺頁面。
3、秒殺持續時間短、瞬時通路流量高。
4、同一使用者/IP禁止秒殺多次。
秒殺系統設計要點:
1、将秒殺系統獨立部署,甚至使用獨立域名,使其與原有網站完全隔離。主要防止秒殺對現有網站業務造成沖擊。
2、頁面靜态化:将活動頁面上的所有可以靜态的元素全部靜态化,并盡量減少動态元素。通過CDN來抗峰值。
3、禁止重複送出:使用者送出之後按鈕置灰,禁止重複送出 使用者限流:在某一時間段内隻允許使用者送出一次請求,比如可以采取IP限流
4、秒殺送出位址在秒殺開始的時候才能得到。
5、使用緩存、隊列。将秒殺數量提前寫入redis中。接收到請求時,直接遞減redis中總數量,如果下單數量為0則請求直接傳回。下單過程失敗則增加redis中總數量。
秒殺架構設計
秒殺系統為秒殺而設計,不同于一般的網購行為,參與秒殺活動的使用者更關心的是如何能快速重新整理商品頁面,在秒殺開始的時候搶先進入下單頁面,而不是商品詳情等使用者體驗細節,是以秒殺系統的頁面設計應盡可能簡單。
商品頁面中的購買按鈕隻有在秒殺活動開始的時候才變亮,在此之前及秒殺商品賣出後,該按鈕都是灰色的,不可以點選。
下單表單也盡可能簡單,購買數量隻能是一個且不可以修改,送貨位址和付款方式都使用使用者預設設定,沒有預設也可以不填,允許等訂單送出後修改;隻有第一個送出的訂單發送給網站的訂單子系統,其餘使用者送出訂單後隻能看到秒殺結束頁面。
要做一個這樣的秒殺系統,業務會分為兩個階段,
第一個階段是秒殺開始前某個時間到秒殺開始
, 這個階段可以稱之為
準備階段
,使用者在準備階段等待秒殺;
第二個階段就是秒殺開始到所有參與秒殺的使用者獲得秒殺結果
, 這個就稱為
秒殺階段
吧。
1 前端層設計
首先要有一個展示秒殺商品的頁面, 在這個頁面上做一個秒殺活動開始的倒計時,
在準備階段内使用者會陸續打開這個秒殺的頁面, 并且可能不停的重新整理頁面
。這裡需要考慮兩個問題:
-
第一個是秒殺頁面的展示
我們知道一個html頁面還是比較大的,
,如果同時有幾千萬人參與一個商品的搶購,一般機房帶寬也就隻有1G~10G,即使做了壓縮,http頭和内容的大小也可能高達數十K,加上其他的css, js,圖檔等資源
,是以這個頁面上網絡帶寬就極有可能成為瓶頸
,由于CDN節點遍布全國各地,能緩沖掉絕大部分的壓力,而且還比機房帶寬便宜~各類靜态資源首先應分開存放,然後放到cdn節點上分散壓力
-
第二個是倒計時
出于性能原因這個一般由js調用用戶端本地時間,就有可能出現用戶端時鐘與伺服器時鐘不一緻,另外伺服器之間也是有可能出現時鐘不一緻。
,這裡考慮一下性能問題,用戶端與伺服器時鐘不一緻可以采用用戶端定時和伺服器同步時間
,就我以前測試的結果來看,一台标準的web伺服器2W+QPS不會有問題,如果100W人同時刷,100W QPS也隻需要50台web,一台硬體LB就可以了~,并且web伺服器群是可以很容易的橫向擴充的(LB+DNS輪詢),這個接口可以隻傳回一小段json格式的資料,而且可以優化一下減少不必要cookie和其他http頭的資訊,是以資料量不會很大,用于同步時間的接口由于不涉及到後端邏輯,隻需要将目前web伺服器的時間發送給用戶端就可以了,是以速度很快
;web伺服器之間時間不同步可以采用統一時間伺服器的方式,一般來說網絡不會成為瓶頸,即使成為瓶頸也可以考慮多機房專線連通,加智能DNS的解決方案
。比如每隔1分鐘所有參與秒殺活動的web伺服器就與時間伺服器做一次時間同步
-
浏覽器層請求攔截
(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%的請求被攔住了。
- 使用者請求分發子產品:使用Nginx或Apache将使用者的請求分發到不同的機器上。
- 使用者請求預處理子產品:判斷商品是不是還有剩餘來決定是不是要處理該請求。
- 使用者請求處理子產品:把通過預處理的請求封裝成事務送出給資料庫,并傳回是否成功。
- 資料庫接口子產品:該子產品是資料庫的唯一接口,負責與資料庫互動,提供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(); } } }