秒殺活動是指網絡商家為促銷等目的組織或網上限時搶購活動,這種活動具有瞬時并發量大、庫存量少和業務邏輯簡單等特點。設計一個秒殺系統需要考慮的因素很多,比如對現有業務的影響、網絡帶寬消耗以及超賣等因素。本文會讨論秒殺系統的各個環節可能存在的問題以及解決方案。
秒殺系統
傻瓜式秒殺系統
秒殺系統的核心難點是并發量,如果不考慮并發問題,那麼我們可以用如下圖所示的簡單的系統結構來實現秒殺系統,用戶隻有兩個簡單操作:刷新界面和秒殺按鈕,服務端也隻有兩個服務接口:返回秒殺界面和處理秒殺邏輯。假設本文中秒殺商品有100個,參與秒殺的用戶有100w個。
但是在高并發場景下,這個系統會有很多問題,我們全文會針對這些問題——進行優化
1.大量用戶同時刷新界面,會對服務器的帶寬造成非常大的壓力;
2.用戶在秒殺前後可以多次重複點擊按鈕,造成很多不必要的請求;
3.用戶可以通過腳本進行搶購,并且搶購成功率非常高;
4.服務端承受高并發請求,會出現響應過慢或失敗等情況;
5.數據庫承受高并發請求,會導緻連接池耗盡和響應緩慢;
6.如果數據庫更新設計得不合理,可能會出現超賣的情況;
秒殺界面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庫存扣減判斷之後,我們已經确保這次請求需要生成訂單,我們就可以通過異步的形式通知訂單服務生成訂單并扣減庫存。
領取方式:
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!