如何設計秒殺系統 如何設計秒殺系統

如何設計秒殺系統

1 秒殺的問題

服務單一、獨立部署

秒殺服務即使自己扛不住高併發而宕機,也不要造成服務雪崩。

秒殺鏈接加密

  • 避免惡意攻擊,機器人模擬秒殺請求
  • 避免鏈接暴露,自己工作人員,提前秒殺商品

庫存預熱、快速扣減

秒殺讀多寫少。無需每次實時校驗庫存。庫存預熱,放到Redis,信號量控制進來秒殺的請求。

動靜分離

Nginx做好動靜分離。靜態資源 Nginx 直接返回,保證秒殺和商品詳情頁的動態請求才打到後端服務集羣。
使用CDN網絡,分擔本集羣壓力。

惡意請求攔截

服務網關識別非法攻擊請求並進行攔截。

流量削峯

使用各種手段,將流量分擔到更大寬度的時間點。比如驗證碼,加入購物車。

限流、熔斷、降級

前端限流+後端限流 限制次數,限制總量,快速失敗降級運行, 熔斷隔離防止雪崩。

隊列削峯

1萬個商品,每個1000件秒殺。雙11 所有秒殺成功的請求,進入隊列,慢慢創建 訂單,扣減庫存即可。

高併發系統設計的三個目標:性能、可用性和可擴展性

在提升系統性能方面我們一直關注的是系統的查詢性能,比如數據庫的分佈式改造,各類緩存。因爲大部分場景都是讀多寫少。

比如一個社區系統初期一定是隻有少量的種子用戶在生產內容,而大部分的用戶都在“圍觀”別人在說什麼。此時,整體流量較小,而寫流量可能只佔整體流量的百分之一,那麼即使整體的QPS到了1w,寫請求QPS也只是到了100,如果要對寫請求做性能優化,性價比不高。

但隨着業務發展,可能遇到一些存在高併發寫請求場景,比如秒殺。假設你的商城策劃了一期秒殺活動,活動在第五天的00:00開始,僅限前200名,那麼秒殺即將開始時,後臺會顯示用戶正在瘋狂地刷新APP或者瀏覽器來保證自己能夠儘量早的看到商品。

但讀請求依舊過高,如何應對?

2 優化方案

丟棄訂單

最早期,量太大扛不住,直接前端隨機reject一些,返回搶單失敗,簡單粗暴,但有效,比如10萬人搶100個iPhone,只要能提前預測有大概1萬以上的人蔘與(通過資格確認、報名等方式收集信息),那麼直接請求進來以後隨機擋回去99%的流量都沒有啥問題。

優化吞吐

中間有段時間,提前準備一大批機器,服務化、分庫分表搞定後端性能,讓前端業務可以加一定量的機器,然後搞穩定性,依賴關係,容量規劃,做彈性,提升吞吐量。

異步隊列

使用可堆積的消息隊列或內存消息隊列。若搶單具有強順序,則先都進隊列,然後拿前N (就是庫存數)個出來平滑處理,剩下都可作爲失敗進行批處理。

甚至還可做一個定長隊列,再往裏寫直接提示失敗。隊列把併發變成串行,從而去掉了分佈式鎖。

內存分配

某些業務可以考慮預熱,提前在每個機器節點內存分配好庫存數,然後直接在內存處理庫存數。

拆分擴展

對於不同類型、不同商家、不同來源的商品,部署不同的前端促銷集羣,
分散壓力。比如,按每個整點發起秒殺,具體到每個商家,其實量就不大了。

服務降級

越重要的搶單,大家越關心自己有沒有搶到,而不是特別在意訂單立即處
理完,也就是說,下單佔到位置比處理完成訂單要更有價值。比如12306春運搶票,只要告訴用戶你搶到了票,但預計1個小時後訂單纔會處理完,用戶有這個明確預期即可。用戶不會立馬使用這張票,也不會在意1min還是1h內處理完。

部分方案會導致銷售不足或超賣:

  • 銷售不足可以從搶購里加一些名單補發,也可以加一輪秒殺
  • 超賣比較麻煩,所以一般會多備一點貨,比如搶100個iPhone,提前準備110 個

因爲用戶查詢的是少量的商品數據,屬查詢熱點數據,可採用緩存將請求儘量擋在上層緩存,能被靜態化的數據(比如商城裏的圖片和視頻數據)儘量做到靜態化,這就可命中CDN節點緩存,減少Web服務器的查詢量和帶寬負擔。Web服務器比如Nginx可以直接訪問分佈式緩存節點,從而避免請求到達Tomcat等業務服務器。

當然,你可以加上一些限流的策略,比如對短時間之內來自某一個用戶、某一個IP或者某一臺設備的重複請求做丟棄處理。

通過這幾種方式,請求就可以儘量擋在數據庫之外了。

稍微緩解了讀請求之後,00:00分秒殺活動準時開始,用戶瞬間向電商系統請求生成訂單,扣減庫存,用戶的這些寫操作都是不經過緩存直達數據庫的。1秒鐘之內,有1萬個數據庫連接同時達到,系統的數據庫瀕臨崩潰,尋找能夠應對如此高併發的寫請求方案迫在眉睫。這時你想到了消息隊列。

理解消息隊列

把消息隊列看作暫時存儲數據的一個容器,它是一個平衡低速系統和高速系統處理任務時間差的工具。

比如古代臣子朝見皇上陳述國家大事,等皇上決策。但大臣很多,如果同時去找皇上,皇上肯定會崩潰。後來變成臣子到午門後要原地等皇上將他們一個一個地召見進大殿商議,這就緩解皇上處理事情的壓力。
可以把午門看作一個暫時容納臣子的容器,即消息隊列:

  • 在Java線程池中我們就會使用一個隊列來暫時存儲提交的任務,等待有空閒的線程處理這些任務
  • os中斷的下半部分也會使用工作隊列來實現延後執行
  • 實現一個RPC框架時,也會將從網絡上接收到的請求寫到隊列裏,再啓動若干個工作線程來處理

那如何用消息隊列解決秒殺場景下的問題呢?

削去秒殺場景下的峯值寫流量

在秒殺場景下短時間之內數據庫的寫流量很高,按以前思路,應該分庫分表。若已做了分庫分表,則需要擴展更多數據庫應對更高寫流量。
但無論是分庫分表還是擴充更多數據庫都會很複雜,因爲你需要遷移數據庫中的數據,這個時間就要按天甚至周計算。

而在秒殺場景下高併發的寫請求並不是持續的,也不是經常發生,而只有在秒殺活動開始後的幾s或十幾s時間內才存在。
爲了應對這十幾s瞬間寫高峯,而去花費幾天甚至幾周擴容DB,再在秒殺之後花費幾天做縮容,得不償失!

所以思路是:將秒殺請求暫存在MQ,然後業務服務器會響應用戶“秒殺結果正在計算”,釋放了系統資源之後再處理其它用戶請求。

在後臺啓動若干個隊列處理程序消費MQ中的消息,再執行校驗庫存、下單等邏輯。因爲只有有限個隊列處理線程在執行,所以落入後端DB上的併發請求有限。而請求是可以在MQ被短暫堆積,當庫存被消耗完後,消息隊列中堆積的請求就可以被丟棄了。

這就是MQ在秒殺系統中主要作用:削峯填谷,可以削平短暫流量高峯,雖說堆積會造成請求被短暫延遲處理,但只要我們時刻監控MQ中的堆積長度,在堆積量超過一定量時,增加隊列處理機數量來提升消息處理能力即可,而且秒殺用戶對於短暫延遲知曉秒殺的結果也有一定容忍度。

注意是“短暫”延遲,若長時間沒有給用戶公示秒殺結果,則用戶會懷疑秒殺活動有黑幕。所以在使用MQ應對流量峯值時,需要對隊列處理的時間、前端寫入流量的大小、數據庫處理能力做好評估,然後根據不同的量級來決定部署多少臺隊列處理程序。

比如你的秒殺商品有1000件,處理一次購買請求的時間是500ms,那麼總共就需要500s的時間。這時你部署10個隊列處理程序,那麼秒殺請求的處理時間就是50s,也就是說用戶需要等待50s纔可以看到秒殺的結果,這是可以接受的。這時會併發10個請求到達數據庫,並不會對數據庫造成很大的壓力。

通過異步處理簡化秒殺請求中的業務流程
其實在大量的寫請求“攻擊”你的電商系統的時候,消息隊列除了發揮主要的削峯填谷的作用之外,還可以實現異步處理來簡化秒殺請求中的業務流程,提升系統的性能。

你想,在剛纔提到的秒殺場景下,我們在處理購買請求時需要500ms。這時你分析了一下整個的購買流程,發現這裏面會有主要的業務邏輯,也會有次要的業務邏輯:比如說,主要的流程是生成訂單、扣減庫存;次要的流程可能是我們在下單購買成功之後會給用戶發放優惠券,會增加用戶的積分。

假如發放優惠券的耗時是50ms,增加用戶積分的耗時也是50ms,那麼如果我們將發放優惠券、增加積分的操作放在另外一個隊列處理機中執行,那麼整個流程就縮短到了400ms,性能提升了20%,處理這1000件商品的時間就變成了400s。如果我們還是希望能在50s之內看到秒殺結果的話,只需要部署8個隊列程序就好了。

經過將一些業務流程異步處理之後,我們的秒殺系統部署結構也會有所改變:

解耦實現秒殺系統模塊之間松耦合
除了異步處理和削峯填谷以外,消息隊列在秒殺系統中起到的另一個作用是解耦合。

比如數據團隊對你說,在秒殺活動之後想要統計活動的數據,藉此來分析活動商品的受歡迎程度、購買者人羣的特點以及用戶對於秒殺互動的滿意程度等等指標。而我們需要將大量的數據發送給數據團隊,那麼要怎麼做呢?

一個思路是:使用HTTP或者RPC的方式來同步地調用,也就是數據團隊這邊提供一個接口,我們實時將秒殺的數據推送給它,但是這樣調用會有兩個問題:

整體系統的耦合性比較強,當數據團隊的接口發生故障時,會影響到秒殺系統的可用性。
當數據系統需要新的字段,就要變更接口的參數,那麼秒殺系統也要隨着一起變更。
這時,我們可以考慮使用消息隊列降低業務系統和數據系統的直接耦合度。

秒殺系統產生一條購買數據後,我們可以先把全部數據發送給消息隊列,然後數據團隊再訂閱這個消息隊列的話題,這樣它們就可以接收到數據,然後再做過濾和處理了。

秒殺系統在這樣解耦合之後,數據系統的故障就不會影響到秒殺系統了,同時當數據系統需要新的字段時,只需要解析消息隊列中的消息,拿到需要的數據就好了。

異步處理、解耦合和削峯填谷是消息隊列在秒殺系統設計中起到的主要作用,其中異步處理可以簡化業務流程中的步驟,提升系統性能;削峯填谷可以削去到達秒殺系統的峯值流量,讓業務邏輯的處理更加緩和;解耦合可以將秒殺系統和數據系統解耦開,這樣兩個系統的任何變更都不會影響到另一個系統,

如果你的系統想要提升寫入性能實現系統的低耦合,想要抵擋高併發的寫流量,那麼你就可以考慮使用消息隊列來完成。

課程小結
本節課,我結合自己的實際經驗,主要帶你瞭解了消息隊列在高併發系統設計中起到的作用以及一些注意事項,你需要了解的重點如下:

削峯填谷是消息隊列最主要的作用,但是會造成請求處理的延遲。
異步處理是提升系統性能的神器,但是你需要分清同步流程和異步流程的邊界,同時消息存在着丟失的風險,我們需要考慮如何確保消息一定到達。
解耦合可以提升你的整體系統的魯棒性。
當然,你要知道,在使用消息隊列之後雖然可以解決現有的問題,但是系統的複雜度也會上升。比如上面提到的業務流程中,同步流程和異步流程的邊界在哪裏?消息是否會丟失,是否會重複?請求的延遲如何能夠減少?消息接收的順序是否會影響到業務流程的正常執行?如果消息處理流程失敗了之後是否需要補發?這些問題都是我們需要考慮的。我會利用接下來的兩節課針對最主要的兩個問題來講講解決思路:一個是如何處理消息的丟失和重複,另一個是如何減少消息的延遲。

引入了消息隊列的同時也會引入了新的問題,需要新的方案來解決,這就是系統設計的挑戰,也是系統設計獨有的魅力,而我們也會在這些挑戰中不斷提升技術能力和系統設計能力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章