這個算是一個經典面試題了,雖說是一個場景題,但是也算是老八股了。
今天就從系統設計的角度來和小夥伴們聊一聊這個話題。
一般來說秒殺系統需要考慮到下面這樣一些問題:
瞬時高併發流量
熱點商品資料
庫存管理
重複下單
黃牛
接下來我們就這裏提到的點逐一進行分析。
本文主要和大家講思路,不講具體做法,具體做法在鬆哥之前的文章中很多已經和大家聊過了。
一 瞬時高併發流量
應對瞬時高併發流量,不是某一種方案就可以,是一個組合拳。另外大家要記得,系統設計沒有銀彈。
1.1 動靜分離部署
這算是一個基本要求了,引入 Nginx,將靜態資源和動態資源利用 Nginx 分流,靜態資源直接返回,動態資源則轉發給後端伺服器去處理。
這一點其實還蠻重要,鬆哥之前就有遇到這個問題,一開始沒有動靜分離部署,後來動靜分離部署之後,系統併發能力提升 2 倍以上。
不過如果願意花點錢,把靜態資源都交給雲服務商的 CDN 來處理,那就更好了。
一般來說使用 CDN 是比較划算的,因為 CDN 流量費往往比雲主機的流量費便宜。
1.2 資料庫獨立部署
這個也算是基操了,將應用程式和資料庫部署到一起,往往無法讓資料庫發揮自己的極限效能。正常來說,一臺 1C2G 的伺服器上只部署 MySQL,就能做到每秒處理 200 次查詢請求,這樣的資料基本上就能滿足一個每天 100W PV 的小網站了。
但是你想想,1C2G 的伺服器部署 MySQL 和應用程式的話,估計卡的沒法用了。
將 MySQL 和應用程式部署到一臺伺服器上,往往會因為兩者互相影響而降低整體的併發效能,具體來說可能會發生這些問題:
高併發導致 CPU 被耗盡,進而 MySQL 響應變慢。
應用程式處理請求的時候需要等待更長的時間獲取資料庫的資料,這個過程佔用了大量的記憶體。
系統記憶體緊張導致 MySQL 中快取的資料被回收,進而拖慢 MySQL。
如此循環往復,系統最終越來越慢甚至崩潰。
因此我們要做的第二件事情就是將資料庫和應用程式獨立分開部署。
1.3 流量過濾
秒殺本來就是一個看運氣的事,誰秒到算誰的,沒秒到就算失敗,產品數量往往有限,秒到的必然是少數人,所以在請求從客戶端到達服務端並處理的過程中,可以對流量進行層層過濾。
一般來說,請求主要經過如下節點:
由於秒殺的隨機性,我們可以這麼做:
Client 處也就是使用者請求發起的地方,我們就可以隨機丟棄一些請求,直接彈出秒殺失敗、網路阻塞等等。
當請求到達 Nginx 之後,可以在 Nginx 處進行限流,利用像 limit_req_zone、limit_req_conn 等模組來實現不同的限流策略。
當請求從 Nginx 上轉發到 Java 服務上之後,我們可以繼續使用一些限流工具,比如 Sentinel,或者自己利用 Redis 寫限流工具也可以,在這裏繼續進行限流。
當請求突破層層關卡到達業務層之後,對於實時性要求不高的資料,直接從快取查詢,快取優先查本地快取,其次是遠端分散式快取如 Redis,快取中沒有資料的話,最後再是 MySQL。
1.4 頁面靜態化
對於熱點資料頁面可以進行靜態化處理。
比如秒殺商品頁、秒殺商品詳情頁等等這些熱點頁面直接自動進行靜態化處理,這樣使用者每次訪問的時候,直接返回現成的頁面,就不用走資料庫了。
如果頁面資料發生變化,重新自動生成靜態頁面即可。
二 熱點商品資料
接下來就是熱點商品資料的處理了。
秒殺這種事情,在秒殺活動開始之前,我們基本上就能夠確定哪些資料是熱點資料了,所以處理處理起來相對來說並不難。
不過需要注意的是,能快取的資料肯定是一些商品資訊類的資料,對於像庫存這類實時性要求極高的資料,是不適合快取的。
2.1 快取預熱
快取預熱主要從兩方面入手:
本地快取預熱
Redis 快取預熱
查詢的時候先查本地快取,沒有再查 Redis 快取,這樣能夠有效避免 Redis 的熱 Key 問題。
2.2 資料拆分
另一方面就是我們要避免熱點資料聚集到一起,將熱點資料進行拆分。避免從一個快取處去獲取多個熱點資料,這樣就能降低快取的壓力。
比如:
商品詳情資料
價格資料
秒殺規則資料
。。。
可以對這些熱點資料進行拆分,其實拆分之後,熱點資料也就不那麼“熱”了。
三 庫存管理
庫存因為實時性要求比較高,因此就不方便用快取。
庫存管理要是做不好,可能會發生超賣或者少賣。
那麼庫存管理怎麼做呢?保險的方案當然就是直接去資料庫扣減,但是資料庫併發能力有限,所以往往還需要結合快取來做。
我們分別來看。
3.1 資料庫扣減
資料庫扣減,爲了避免把庫存扣成負數,一般來說我們有兩種思路:
悲觀鎖
樂觀鎖
在高併發場景下,悲觀鎖會導致更新效率降低很多;而樂觀鎖則會導致大量的失敗。似乎都不是一個很好的選擇。
其實我們只是要保證庫存不被減為負數而已,那麼其實就可以在更新 SQL 中新增一個條件就行了,像下面這樣:
***** and 庫存>=0
大致上這樣就可以了。
不過只是這樣做還不夠,因為資料庫的併發能力在哪擺著呢。所以我們還是要利用快取。
3.2 快取扣減
由於 Redis 本身就是單執行緒執行的,因此我們再結合上 Lua 指令碼,就可以保證扣減庫存這個操作的原子性。
在 Lua 指令碼中我們可以獲取到庫存資料,然後判斷庫存,沒問題再進行扣減。
Redis 本身的高效能+單執行緒執行+Lua 指令碼的原子性,這三點結合起來就可以確保上述操作是沒有問題的。
3.3 最佳實踐
在具體實踐中,往往是 3.1 和 3.2 結合起來。
具體流程是這樣:
首先 Redis 做扣減,扣減完了之後,傳送一條訊息給 MQ,應用程式再去消費這條訊息,消費訊息時完成資料庫的扣減。
這個過程中我們需要確保好 MQ 訊息的可靠性和冪等性,處理好訊息積壓。
當然,穩妥起見還需要有對賬機制,定時拉取 Redis 中的資料和資料庫中的資料進行對比,保證資料的一致性。
四 重複下單
秒殺場景下使用者由於比較焦急,頻繁點選可能造成重複下單,因此我們需要處理好下單操作的冪等性。
這個也有很多思路,需要多管齊下。
4.1 前端置灰
前端使用者點選之後,就對秒殺按鈕進行置灰操作,同時提醒使用者目前正在進行秒殺。
這是基操,但是不能從根本上解決問題,還得配合後段冪等性處理。
4.2 後端冪等性處理
後段冪等性處理有很多方案,可以利用 Token 機制,這個鬆哥之前也有很多文章介紹,不多說。
同時因為秒殺這種場景往往是限購的,因此在使用者下單的時候可以判斷是否有在途訂單或者使用者是否已經下單,進而決定當前下單操作是否能夠成功。
五 黃牛
薅羊毛的黃牛也是我們要考慮的一個問題。
5.1 識別黃牛
首先我們要識別出來哪些使用者可能是黃牛,一般來說,我們可以透過如下方式來識別:
請求頻率:監測使用者的請求頻率,若某一賬戶的請求過於頻繁,則可能是黃牛使用自動化工具發出的。
訪問模式:分析使用者的訪問模式,例如短時間內大量的重複請求或者非正常人類行為的訪問模式。
IP 地址:檢查請求來源的 IP 地址,對於同一 IP 地址下頻繁的請求進行限制或標記。
如果公司有足夠的人力資源,這塊可以建立預測模型,透過模型去分析哪些人可能是黃牛。
5.2 防止黃牛
當我們識別出來黃牛之後,一般來說有如下一些辦法:
圖形驗證碼(CAPTCHA):在關鍵環節加入圖形驗證碼,要求使用者識別並輸入相應的字元,以防止自動化工具的使用。
滑動驗證:在關鍵環節採用滑動驗證等互動式驗證方式,這類驗證方式難以被自動化工具模擬,這也是大家目前見到的最多的驗證方式了。
行為驗證:基於使用者的行為軌跡(如滑鼠移動軌跡、鍵盤輸入模式等)來進行驗證,這個目前鬆哥只在京東圖書上見過這種驗證方式。
請求頻率限制:對識別出來的使用者或 IP 地址的請求頻率進行限制,超出限制則暫時禁止訪問,這塊利用 Nginx 或者 Sentinel 就能實現。
黑名單:對於已知的黃牛 IP 地址或賬戶進行封禁處理,這塊可以直接在 Nginx 上處理,也可以在閘道器如 Spring Cloud Gateway 上處理。
動態調整:根據系統的實時負載情況動態調整限流閾值。
六 小結
秒殺是一個大工程,以上是鬆哥和大家分享的一些實現思路,具體落實下來還有很多細節需要處理。
藉助本文希望小夥伴們在面試的時候不怯場,能夠回答出來。
歡迎小夥伴們在評論區分享自己的方案或者提出補充。