本章主要介紹高併發業務(秒殺活動)系統是如何設計的。
設計針對於該虛擬命題:有10000件商品,每個使用者最多購買2件。五分鐘未付款直接退單,可手動退單。
在看設計方案時我們先來整理下秒殺活動會帶來什麼樣的高併發
- 秒殺開始前:可能會有大量請求商品詳情頁。
- 秒殺進行時:大量請求下單。
請求流控
第一層 :設定瀏覽器快取與CDN
商品詳情頁相關介面設定瀏覽器快取。相關圖片,CSS,JS一些靜態資源儲存CDN。
瀏覽器快取的設計方案:設定1分鐘的強制快取,然後通過協商快取你判斷該快取是否有效。(既能限流也能即使更新頁面最新情況)
瀏覽器快取:當你請求HTTP請求後收到一個HTTP響應體的時候,瀏覽器會判斷響應頭中是否有快取的標識,如果有,則會把請求內容存入硬碟中(或者記憶體中),下次的準備發起相同請求時瀏覽器會自行判斷快取內容是否有效,如果有效則不進行請求,直接獲取快取內容。
CDN:把一些靜態資源交由第三方平臺儲存。這樣請求時無需訪問自己的伺服器並且響應速度也比較快。
第二層 :設定介面請求頻率
該設定主要是過濾一些非常規可能請求(模擬請求)。
- 前端JS控制下單按鈕不能重複點選。
-
通過nginx伺服器限流配置使同一IP頻率限制,如果超過限制則返回500狀態碼。
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; location /login/ { limit_req zone=mylimit burst=20 nodelay; limit_req_status 444; } // limit_req_zone 限流的配置 // $binary_remote_addr 為用ip地址, zone=mylimit:10m 開闢一個10M的名為mylimit的空間 rate=10r/s 每秒10請求 // limit_req //使用限流配置 // burst=20 //接受20個快取請求, nodelay 不延遲執行
提示業務響應速度
通過請求流程可以過濾無效請求,但是如果使用者體量太大,通過流控仍有大量請求。
此時我們就需要加快程式的處理速度進而提升請求的響應速度,響應速度提升後單位時間內處理的請求就變多了。
讀優化
通過redis快取秒殺過程中會用到的資料。
寫優化
解耦相關的入庫操作通過訊息佇列中介軟體RabbitMQ
邏輯流程優化
及時響應無效的請求(越容易判斷寫在越前面)
具體實現流程
下單介面邏輯流程
//redis 相關資料的格式定義
'is_start' => 0 // 0 未開始,1開始, 2結束
'buy:'.$userID.':'.$goodID => 0 // userID使用者已購買goodID商品的數量
'stock'.$goodID => {
'stock' => 10000 // 商品庫存量
'sales' => 0 //已售量
}
'order':$userID:$order_no => {
訂單資訊
}
- 判斷秒殺活動是否開啟
- 判斷使用者是否還能購買
checkBuy
指令碼返回0表示不能購買 - 判斷庫存是否滿足
checkStock
指令碼返回0表示無庫存,無庫存的時候要把使用者已購買數量新增回去 - 將訂單請求放入下單佇列,並且響應訂單號給前端。 訊息佇列內容 使用者id, 商品id,商品數量,訂單號。
- 非同步消費下單佇列,寫入資料庫,並且寫入redis 'order':$order_on。
- 如果成功redis儲存訂單資訊,並且新增過期佇列延遲5分鐘。過期佇列內容 使用者id, 商品id,商品數量,訂單號。
- 如果失敗儲存失敗原因(釋放庫存 'stock'.$goodID,釋放已購買數 'buy:'.$userID.':'.$goodID )。
*非同步消費過期佇列,如果已過期則更改訂單狀態,釋放庫存 'stock'.$goodID,釋放已購買數 'buy:'.$userID.':'.$goodID
獲取訂單介面邏輯流程
- 通過請求的order_no與使用者身份標識去redis 查詢 'order':$userID:$order_no資料並返回。
取消訂單與退單介面
- 更改訂單狀態,釋放庫存 'stock'.$goodID,釋放已購買數 'buy:'.$userID.':'.$goodID
前端接收下單響應結果處理
-
如果響應狀態嗎!200, 提示搶購失敗
-
如果是200響應,下單介面會返回order_no。前端可以展示訂單建立中。
- 然後ajax去輪詢獲取訂單介面,請求引數order_no.獲取redis資訊。如果成功則展示結算頁。如果失敗則展示失敗原因。
結算頁
- 支付選擇收貨地址並且進行支付。
訂單頁
- 可以進行取消訂單,退單
//checkBuy指令碼
script load "lua code"
//lua code
local n = tonumber(ARGV[1]);
if not n or n == 0 then
return 0
end
local val = tonumber(redis.call('GET', KEYS[1]));
if (not val) or (val + n <= 2) then
redis.call('INCRBY', KEYS[1], n);
return n;
end
return 0;
//49d185704d033c30e29b09a72e687d4c322e7801
evalsha 49d185704d033c30e29b09a72e687d4c322e7801 1 'buy:'.$userID.':'.$goodID 1
//checkStock指令碼
script load "lua code"
//lua code
local n = tonumber(ARGV[1])
if not n or n == 0 then
return 0
end
local vals = redis.call('HMGET', KEYS[1], 'stock', 'sales');
local stock = tonumber(vals[1])
local sales = tonumber(vals[2])
if not stock or not sales then
return 0
end
if sales + n <= stock then
redis.call('HINCRBY', KEYS[1], 'sales', n)
return n;
end
return 0
//51046114c9b5b102554a381969343493e29dcb34
evalsha 51046114c9b5b102554a381969343493e29dcb34 1 'stock'.$goodID 1