博客 / 詳情

返回

如果讓你設計一個秒殺系統,你會怎麼做?

這個算是一個經典面試題了,雖説是一個場景題,但是也算是老八股了。

今天就從系統設計的角度來和小夥伴們聊一聊這個話題。

一般來説秒殺系統需要考慮到下面這樣一些問題:

  1. 瞬時高併發流量
  2. 熱點商品數據
  3. 庫存管理
  4. 重複下單
  5. 黃牛

接下來我們就這裏提到的點逐一進行分析。

本文主要和大家講思路,不講具體做法,具體做法在鬆哥之前的文章中很多已經和大家聊過了。

一 瞬時高併發流量

應對瞬時高併發流量,不是某一種方案就可以,是一個組合拳。另外大家要記得,系統設計沒有銀彈。

1.1 動靜分離部署

這算是一個基本要求了,引入 Nginx,將靜態資源和動態資源利用 Nginx 分流,靜態資源直接返回,動態資源則轉發給後端服務器去處理。

這一點其實還蠻重要,鬆哥之前就有遇到這個問題,一開始沒有動靜分離部署,後來動靜分離部署之後,系統併發能力提升 2 倍以上

不過如果願意花點錢,把靜態資源都交給雲服務商的 CDN 來處理,那就更好了。

一般來説使用 CDN 是比較划算的,因為 CDN 流量費往往比雲主機的流量費便宜。

1.2 數據庫獨立部署

這個也算是基操了,將應用程序和數據庫部署到一起,往往無法讓數據庫發揮自己的極限性能。正常來説,一台 1C2G 的服務器上只部署 MySQL,就能做到每秒處理 200 次查詢請求,這樣的數據基本上就能滿足一個每天 100W PV 的小網站了。

但是你想想,1C2G 的服務器部署 MySQL 和應用程序的話,估計卡的沒法用了。

將 MySQL 和應用程序部署到一台服務器上,往往會因為兩者互相影響而降低整體的併發性能,具體來説可能會發生這些問題:

  1. 高併發導致 CPU 被耗盡,進而 MySQL 響應變慢。
  2. 應用程序處理請求的時候需要等待更長的時間獲取數據庫的數據,這個過程佔用了大量的內存。
  3. 系統內存緊張導致 MySQL 中緩存的數據被回收,進而拖慢 MySQL。
  4. 如此循環往復,系統最終越來越慢甚至崩潰。

因此我們要做的第二件事情就是將數據庫和應用程序獨立分開部署。

1.3 流量過濾

秒殺本來就是一個看運氣的事,誰秒到算誰的,沒秒到就算失敗,產品數量往往有限,秒到的必然是少數人,所以在請求從客户端到達服務端並處理的過程中,可以對流量進行層層過濾。

一般來説,請求主要經過如下節點:

由於秒殺的隨機性,我們可以這麼做:

  1. Client 處也就是用户請求發起的地方,我們就可以隨機丟棄一些請求,直接彈出秒殺失敗、網絡阻塞等等。
  2. 當請求到達 Nginx 之後,可以在 Nginx 處進行限流,利用像 limit_req_zone、limit_req_conn 等模塊來實現不同的限流策略。
  3. 當請求從 Nginx 上轉發到 Java 服務上之後,我們可以繼續使用一些限流工具,比如 Sentinel,或者自己利用 Redis 寫限流工具也可以,在這裏繼續進行限流。
  4. 當請求突破層層關卡到達業務層之後,對於實時性要求不高的數據,直接從緩存查詢,緩存優先查本地緩存,其次是遠程分佈式緩存如 Redis,緩存中沒有數據的話,最後再是 MySQL。

1.4 頁面靜態化

對於熱點數據頁面可以進行靜態化處理。

比如秒殺商品頁、秒殺商品詳情頁等等這些熱點頁面直接自動進行靜態化處理,這樣用户每次訪問的時候,直接返回現成的頁面,就不用走數據庫了。

如果頁面數據發生變化,重新自動生成靜態頁面即可。

二 熱點商品數據

接下來就是熱點商品數據的處理了。

秒殺這種事情,在秒殺活動開始之前,我們基本上就能夠確定哪些數據是熱點數據了,所以處理處理起來相對來説並不難。

不過需要注意的是,能緩存的數據肯定是一些商品信息類的數據,對於像庫存這類實時性要求極高的數據,是不適合緩存的。

2.1 緩存預熱

緩存預熱主要從兩方面入手:

  1. 本地緩存預熱
  2. Redis 緩存預熱

查詢的時候先查本地緩存,沒有再查 Redis 緩存,這樣能夠有效避免 Redis 的熱 Key 問題。

2.2 數據拆分

另一方面就是我們要避免熱點數據聚集到一起,將熱點數據進行拆分。避免從一個緩存處去獲取多個熱點數據,這樣就能降低緩存的壓力。

比如:

  • 商品詳情數據
  • 價格數據
  • 秒殺規則數據
  • 。。。

可以對這些熱點數據進行拆分,其實拆分之後,熱點數據也就不那麼“熱”了。

三 庫存管理

庫存因為實時性要求比較高,因此就不方便用緩存。

庫存管理要是做不好,可能會發生超賣或者少賣。

那麼庫存管理怎麼做呢?保險的方案當然就是直接去數據庫扣減,但是數據庫併發能力有限,所以往往還需要結合緩存來做。

我們分別來看。

3.1 數據庫扣減

數據庫扣減,為了避免把庫存扣成負數,一般來説我們有兩種思路:

  1. 悲觀鎖
  2. 樂觀鎖

在高併發場景下,悲觀鎖會導致更新效率降低很多;而樂觀鎖則會導致大量的失敗。似乎都不是一個很好的選擇。

其實我們只是要保證庫存不被減為負數而已,那麼其實就可以在更新 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 識別黃牛

首先我們要識別出來哪些用户可能是黃牛,一般來説,我們可以通過如下方式來識別:

  1. 請求頻率:監測用户的請求頻率,若某一賬户的請求過於頻繁,則可能是黃牛使用自動化工具發出的。
  2. 訪問模式:分析用户的訪問模式,例如短時間內大量的重複請求或者非正常人類行為的訪問模式。
  3. IP 地址:檢查請求來源的 IP 地址,對於同一 IP 地址下頻繁的請求進行限制或標記。

如果公司有足夠的人力資源,這塊可以建立預測模型,通過模型去分析哪些人可能是黃牛。

5.2 防止黃牛

當我們識別出來黃牛之後,一般來説有如下一些辦法:

  • 圖形驗證碼(CAPTCHA):在關鍵環節加入圖形驗證碼,要求用户識別並輸入相應的字符,以防止自動化工具的使用。
  • 滑動驗證:在關鍵環節採用滑動驗證等交互式驗證方式,這類驗證方式難以被自動化工具模擬,這也是大家目前見到的最多的驗證方式了。
  • 行為驗證:基於用户的行為軌跡(如鼠標移動軌跡、鍵盤輸入模式等)來進行驗證,這個目前鬆哥只在京東圖書上見過這種驗證方式。
  • 請求頻率限制:對識別出來的用户或 IP 地址的請求頻率進行限制,超出限制則暫時禁止訪問,這塊利用 Nginx 或者 Sentinel 就能實現。
  • 黑名單:對於已知的黃牛 IP 地址或賬户進行封禁處理,這塊可以直接在 Nginx 上處理,也可以在網關如 Spring Cloud Gateway 上處理。
  • 動態調整:根據系統的實時負載情況動態調整限流閾值。

六 小結

秒殺是一個大工程,以上是鬆哥和大家分享的一些實現思路,具體落實下來還有很多細節需要處理。

藉助本文希望小夥伴們在面試的時候不怯場,能夠回答出來。

歡迎小夥伴們在評論區分享自己的方案或者提出補充。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.