秒殺系統的設計
現在許多商家了吸引顧客都會使用低價的秒殺商品來做活動,下圖展示了京東的秒殺活動頁面:
秒殺活動在開始的時候,用戶根據自己的需要下單自己喜歡的商品,此時服務器瞬間會打入大量的流量進來,如何設計一套可以扛住高併發的秒殺系統來支撐秒殺活動?下面我們具體的做秒殺系統的設計。
1、整體的秒殺設計
秒殺系統中我們使用了 LVS 和 Nginx 來扛住第一波流量,並且在 Nginx 做一波限流控制;然後 Nginx 將請求轉發到網關上,由網關將請求分發到處理服務器上(秒殺服務),接下來由處理服務器做相關的操作。下單中使用的是異步下單加 WebSocket 推送下單成功的消息到客戶端上,使用 Redis 緩存來緩存商品信息、預庫存信息和用戶下單成功之後的訂單信息。
2、定時上架商品和預庫存
在秒殺活動中爲了提高秒殺系統的性能,將秒殺商品、商品的庫存信息提前添加到 Redis 中。
在秒殺活動的當天凌晨,將秒殺商品信息和庫存都放到 Redis 中緩存起來,由於每場秒殺活動有不同的商品,所以針對不同的場次將商品信息使用 Hash 數據結構緩存起來。
3、秒殺商品展示
秒殺活動開始的時候,用戶訪問商品的頁的時候,直接從 redis 中將緩存的商品信息給用戶,這樣就不用走數據庫查詢,提高了系統的吞吐量。
4、用戶下單
4.1 每場每個商品用戶只能下一單的處理
用戶下單的時候,首先的要去判斷一下用戶本場是否已經下過單(限定每場活動的每個商品用戶只能買一件,防止某個用戶同時多個請求來到秒殺系統,爲此在 Redis 中增加一個 Set 數據結構來記錄用戶本場針對某個商品已經下過單。如果 Set 中存在了本場次當前商品已經下過單就不可以讓用戶繼續下單,反之可以下單。
但是高併發下此方式不能完全避免用戶多次下單的問題,如下:
在 t1 時刻,兩個線程同時請求用戶的下單,此時發現沒有當前的用戶沒有下單,放行兩個線程到庫存檢查這一步,此時如果庫存足夠會讓線程下單,那麼就造成了同一個用戶針對這個商品就多次下單了。解決方案就是在數據庫中增加一個唯一鍵來兜底。唯一鍵可以使用時間 + 商品的 id + 場次來定義,如果數據插入失敗就需要回補庫存到 Redis 中。
4.2 商品預庫存
商品的預庫存主要作用是阻擋大部分的無效請求,因爲秒殺商品往往數量比實際用戶的請求數量小很多(如秒殺商品 100 個,用戶在某個時間段中來了 500 個請求,此時真正要處理的請求其實就是 100 個,其餘的 400 個請求多是多餘的請求,因爲庫存不足無法下單),使用預庫存可以讓無效請求直接返回而不需要到訂單服務中的處理。
如果存在用戶下單後沒有付款或者同一個用戶多個請求的情況下,會出現預庫存被佔用導致想要購買的用戶無法下單,此時需要回補預庫存。回補庫存我們直接採用將數據庫的剩餘庫存數量直接放入到預庫存隊列中。
高併發這種方案可能會出現預庫存數量比實際真實的剩餘庫存要多的情況,如下:
A 線程和 B 線程回補庫存的時候,此時發現真實庫存是 100 個,B 線程 CPU 的時間片恰好用完了,A 線程首先執行回補 100 個庫存到 Redis,然後 C 下單成功後扣減了 1 個預庫存,隨後 B 線程獲得了時間片,然後將 100 庫存又回補到 Redis 中,此時真實的庫存其實是 99 個,但是 B 線程回補多 1 個預庫存;針對這種情況需要數據庫層做兜底來保證不會超賣(創建訂單的時候兜底處理)。
4.3 用戶異步下單
如果用戶在本場本商品沒有下單,並且預庫存扣減成功(實質是利用 Redis 的自減操作,如果自減後的結果大於 0 就庫存是足夠的)那麼允許用戶下單,此時我們利用 MQ 來異步下單。
MQ 會發送兩個消息,一個是實時的下單消息,一個是延遲的檢查規定時間用戶是否支付的消息(如果超時未支付就取消本單,並且回補預庫存);MQ 消息發送成功之後通知客戶已經在下單隊列中等待處理。
訂單服務開始消費消息隊列中的消息,首先會創建訂單基本的信息、下單的商品信息等,創建成功之後訂單信息會保存到數據庫中並且數據還會同步一份到 Redis 中用於用戶查詢訂單的信息。最後通過 WebSocket 技術將用戶下單的成功的消息推送給客戶端,方便用戶支付。
給用戶創建訂單的時候底層需要使用樂觀鎖機制,先去扣減秒殺商品的庫存(如 update item set stock = stock - 1 where id = #{itemId} and stock > 0),如果扣減影響行數大於 0,那麼就代表扣減庫存成功,可以讓用戶下單,這樣可以保證商品不被超賣。
延遲消息(如延遲 15 分鐘)過來之後,查詢一下當前訂單是否已經被支付,如果訂單沒有支付就將訂單的狀態修改成交易關閉。在修改訂單的狀態的中,極端情況下出現超時未支付的請求和用戶支付請求同時來操作訂單,如下:
此時如果不做預防措施,訂單就會出現狀態錯亂的現象,爲了解決這個問題,採用訂單的狀態機來處理。如下狀態的轉換:
超時未支付的請求和用戶支付請求誰先操作誰爲準(底層的 Sql 語句:update order set status = #{status} where order_id = #{orderId} and status = '待支付'),這樣另一個線程的就操作失敗,我們需要打印日誌拋出異常提示。
5、訂單支付
服務端創建訂單成功之後,用戶開始支付,支付走三方(如支付寶、微信)支付成功後需要及時修改訂單的狀態和同步 Redis 中訂單的狀態。
總結:
(1)秒殺系統設計中採用 LVS+Nginx 方式來提高系統的併發能力
(2)採用將商品信息、商品庫存緩存 Redis 的方式來提高系統的響應和攔截無效的請求
(3)通過異步下單(MQ)的方式來給流量消峯處理,維持系統的穩定
(4)採用 WebSocket 的方式將下單成功的消息推送給客戶端
(5)超賣問題採用樂觀鎖的機制做兜底處理
(6)針對用戶超時未支付或者多次下單同一個商品導致商品少賣的問題,採用真實庫存回補的方式來處理預庫存(Redis 的中的庫存)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/zCbRiA6c9phXs5BJ-hqTfQ