秒殺活動是指網路商家為促銷等目的組織會網上限時搶購活動,這種活動具有瞬時併發量大、庫存量少和業務邏輯簡單等特點。設計一個秒殺系統需要考慮的因素很多,比如對現有業務的影響、網路頻寬消耗以及超賣等因素。本文會討論秒殺系統的各個環節可能存在的問題以及解決方案。
秒殺系統
傻瓜式秒殺系統
秒殺系統的核心難點是併發量,如果不考慮併發問題,那麼我們可以用如下圖所示的簡單的系統結構來實現秒殺系統,使用者只有兩個簡單操作:重新整理介面和秒殺按鈕,服務端也只有兩個服務介面:返回秒殺介面和處理秒殺邏輯。假設本文中秒殺商品有100個,參與秒殺的使用者有100w個。
但是在高併發場景下,這個系統會有很多問題,我們全文會針對這些問題一一進行優化
- 大量使用者同時重新整理介面,會對伺服器的頻寬造成非常大的壓力;
- 使用者在秒殺前後可以多次重複點選按鈕,造成很多不必要的請求;
- 使用者可以通過指令碼進行搶購,並且搶購成功率非常高;
- 服務端承受高併發請求,會出現響應過慢或失敗等情況;
- 資料庫承受高併發請求,會導致連線池耗盡和響應緩慢;
- 如果資料庫更新設計的不合理,可能會出現超賣的情況;
秒殺介面CDN
秒殺開始之前,使用者都會請求秒殺介面,有的使用者甚至會不斷的重新整理秒殺介面,100W使用者可能產生上千萬次秒殺介面請求。秒殺介面往往包含很多靜態資源,如果這些介面請求全部通過伺服器獲取,會造成大量的頻寬消耗,甚至造成秒殺還沒開始伺服器就崩了的情況。
對於網頁這種靜態資源的併發訪問,業內早就有成熟的解決方案:內容分發網路(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個請求,大大減少了伺服器端的壓力。
Redis快取
如果通過前面的過濾,請求量依舊非常大,如果資料庫無法處理這些請求量,我們就需要在資料庫之上新增一層Redis快取了。單個Redis可以處理幾萬的QPS,如果預估請求的QPS大於幾萬,我們還可以使用Redis叢集模式來增加Redis的處理能力。
在Redis存放和售賣商品數目大小相同的數字,秒殺服務每次訪問資料庫之前,都需要先去Redis中扣減庫存,扣減成功才能繼續更新資料庫。這樣,最終到的資料庫的請求數目和需要售賣商品的數目基本一致,資料庫的壓力可以大大減少。
Redis原子性
我們知道Redis是不支援事務的,所以可能出現扣減為負數的情況,這種情況下我們可以使用Lua指令碼來保證一次扣減操作的原子性,從而保證扣減結果的正確性。
非同步更新資料庫
通過Redis判斷之後,去更新資料庫的請求都是必要的請求,這些請求資料庫必須要處理,但是如果資料庫還是處理不過來這些請求怎麼辦呢?
這個時候就可以考慮削峰填谷操作了,削峰填谷最好的實踐就是MQ了。經過Redis庫存扣減判斷之後,我們已經確保這次請求需要生成訂單,我們就可以通過非同步的形式通知訂單服務生成訂單並扣減庫存。
我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd
本文最先發布至微信公眾號,版權所有,禁止轉載!