秒殺系統設計


秒殺活動是指網絡商家為促銷等目的組織會網上限時搶購活動,這種活動具有瞬時並發量大、庫存量少和業務邏輯簡單等特點。設計一個秒殺系統需要考慮的因素很多,比如對現有業務的影響、網絡帶寬消耗以及超賣等因素。本文會討論秒殺系統的各個環節可能存在的問題以及解決方案。

秒殺系統

傻瓜式秒殺系統

秒殺系統的核心難點是並發量,如果不考慮並發問題,那么我們可以用如下圖所示的簡單的系統結構來實現秒殺系統,用戶只有兩個簡單操作:刷新界面和秒殺按鈕,服務端也只有兩個服務接口:返回秒殺界面和處理秒殺邏輯。假設本文中秒殺商品有100個,參與秒殺的用戶有100w個。

傻瓜式秒殺系統

但是在高並發場景下,這個系統會有很多問題,我們全文會針對這些問題一一進行優化

  1. 大量用戶同時刷新界面,會對服務器的帶寬造成非常大的壓力;
  2. 用戶在秒殺前后可以多次重復點擊按鈕,造成很多不必要的請求;
  3. 用戶可以通過腳本進行搶購,並且搶購成功率非常高;
  4. 服務端承受高並發請求,會出現響應過慢或失敗等情況;
  5. 數據庫承受高並發請求,會導致連接池耗盡和響應緩慢;
  6. 如果數據庫更新設計的不合理,可能會出現超賣的情況;

秒殺界面CDN

秒殺開始之前,用戶都會請求秒殺界面,有的用戶甚至會不斷的刷新秒殺界面,100W用戶可能產生上千萬次秒殺界面請求。秒殺界面往往包含很多靜態資源,如果這些界面請求全部通過服務器獲取,會造成大量的帶寬消耗,甚至造成秒殺還沒開始服務器就崩了的情況。

對於網頁這種靜態資源的並發訪問,業內早就有成熟的解決方案:內容分發網絡(CDN)。我們可以在秒殺開始前,預先把網頁的靜態資源存放在CDN節點,用戶在刷新界面時直接從CDN獲取靜態資源,從而降低刷新秒殺界面對服務器造成的壓力。添加了CDN服務之后,秒殺界面有大量用戶同時訪問和刷新並不會給服務端帶來多大壓力。

秒殺界面CDN

秒殺按鈕優化

我們知道,秒殺系統往往會有一個秒殺按鈕,如果不對按鈕限制,可能存在以下問題:

  • 用戶在秒殺開始前點擊按鈕,造成很多無用請求;
  • 用戶在秒殺開始后多次點擊按鈕,造成很多重復請求;

所以我們可以對按鈕做一些限制:秒殺開始前按鈕不可用,用戶點擊一次秒殺按鈕后,按鈕也進入不可用狀態。這種方式無法限制通過腳本請求后端的情況,但是可以限制正常用戶的多次無效點擊,大大降低請求量。

秒殺按鈕優化

秒殺鏈接優化

普通情況下,用戶在點擊秒殺按鈕的時候,前端會請求一個固定的URL,這個URL可以在前端界面查到。對於普通不懂技術的用戶來說,這沒有什么問題,如果用戶稍微懂點Http協議,就可以在秒殺開始前拿到URL,在秒殺開始前或開始的毫秒級時間內請求秒殺鏈接,不僅會給服務端帶來很大的壓力,還會造成不公平現象:商品都被開腳本的人搶走了。

為了避免這種現象,我們可以將URL動態化,即使秒殺系統的開發人員也無法在知曉在秒殺開始時的URL。具體實現方法是在獲取秒殺URL的接口中,返回一個服務器端生成的隨機數,並在下單URL中傳遞該參數完成下單。

秒殺鏈接優化

秒殺驗證碼

雖然說我上面通過動態URL避免了用戶在秒殺開始前請求秒殺鏈接,但是用戶還是可以通過腳本在秒殺開始的那一刻去請求秒殺連接,普通用戶基本沒有辦法和腳本秒殺進行競爭。

我們可以引入機器難以識別的驗證碼,用戶在請求秒殺鏈接之前,需要填寫驗證碼識別的結果,驗證碼錯誤的請求直接拒絕。使用驗證碼不僅可以增加腳本秒殺的難度,還可以降低請求的QPS,因為請求不再是在秒殺那一刻進來,而會被分散到填寫驗證碼的時間段內。

秒殺按鈕優化

過濾請求

通過上面的步驟,我們可以減少很多重復請求和腳本請求,可以保證秒殺活動中一個人大致只會請求一次(腳本還是可以請求多次)。但是100W人參與秒殺,每人請求一次秒殺鏈接也有將近100W次請求,服務器還是扛不住。

仔細分析之后可以發現,秒殺的商品只有100個,最后成功的也只有100個,那么我們100W的請求是不是都有必要請求到秒殺服務器上呢?顯而易見,我們沒有必要把所有請求都打到秒殺服務器上,我們只需要保證有大於100個請求打到秒殺服務器就可以保證秒殺的正常進行,所以我們可以在用戶端和服務端添加一層過濾層,過濾層只要保證有100個以上的請求能打到秒殺服務器端。

我們可以使用Nginx服務器來構建過濾層,一個Nginx服務器也沒法抗100W的請求,我們假設每個Nginx服務器可以處理10W的請求,那么我們就需要10台Nginx。那么怎么用保證至少有100個請求可以請求到后端呢?我們可以簡單的讓每個Nginx服務器只通過前100個請求,后續請求直接返回降級界面。通過Nginx過濾,我們可以把100W的請求過濾為1000個請求,大大減少了服務器端的壓力。

Nginx過濾請求

Redis緩存

如果通過前面的過濾,請求量依舊非常大,如果數據庫無法處理這些請求量,我們就需要在數據庫之上添加一層Redis緩存了。單個Redis可以處理幾萬的QPS,如果預估請求的QPS大於幾萬,我們還可以使用Redis集群模式來增加Redis的處理能力。

在Redis存放和售賣商品數目大小相同的數字,秒殺服務每次訪問數據庫之前,都需要先去Redis中扣減庫存,扣減成功才能繼續更新數據庫。這樣,最終到的數據庫的請求數目和需要售賣商品的數目基本一致,數據庫的壓力可以大大減少。

Redis原子性

我們知道Redis是不支持事務的,所以可能出現扣減為負數的情況,這種情況下我們可以使用Lua腳本來保證一次扣減操作的原子性,從而保證扣減結果的正確性。

Redis緩存

異步更新數據庫

通過Redis判斷之后,去更新數據庫的請求都是必要的請求,這些請求數據庫必須要處理,但是如果數據庫還是處理不過來這些請求怎么辦呢?

這個時候就可以考慮削峰填谷操作了,削峰填谷最好的實踐就是MQ了。經過Redis庫存扣減判斷之后,我們已經確保這次請求需要生成訂單,我們就可以通過異步的形式通知訂單服務生成訂單並扣減庫存。

異步更新數據庫

我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

本文最先發布至微信公眾號,版權所有,禁止轉載!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM