如何設計秒殺系統(tǒng)
1 秒殺的問題
服務單一荒叶、獨立部署
秒殺服務即使自己扛不住高并發(fā)而宕機幻林,也不要造成服務雪崩。
秒殺鏈接加密
- 避免惡意攻擊酪惭,機器人模擬秒殺請求
- 避免鏈接暴露,自己工作人員者甲,提前秒殺商品
庫存預熱春感、快速扣減
秒殺讀多寫少。無需每次實時校驗庫存虏缸。庫存預熱鲫懒,放到Redis,信號量控制進來秒殺的請求刽辙。
動靜分離
Nginx做好動靜分離窥岩。靜態(tài)資源 Nginx 直接返回,保證秒殺和商品詳情頁的動態(tài)請求才打到后端服務集群宰缤。
使用CDN網(wǎng)絡颂翼,分擔本集群壓力。
惡意請求攔截
服務網(wǎng)關識別非法攻擊請求并進行攔截慨灭。
流量削峰
使用各種手段朦乏,將流量分擔到更大寬度的時間點。比如驗證碼氧骤,加入購物車呻疹。
限流、熔斷筹陵、降級
前端限流+后端限流 限制次數(shù)刽锤,限制總量镊尺,快速失敗降級運行, 熔斷隔離防止雪崩姑蓝。
隊列削峰
1萬個商品鹅心,每個1000件秒殺。雙11 所有秒殺成功的請求纺荧,進入隊列旭愧,慢慢創(chuàng)建 訂單,扣減庫存即可宙暇。
高并發(fā)系統(tǒng)設計的三個目標:性能输枯、可用性和可擴展性。
在提升系統(tǒng)性能方面我們一直關注的是系統(tǒng)的查詢性能占贫,比如數(shù)據(jù)庫的分布式改造桃熄,各類緩存。因為大部分場景都是讀多寫少型奥。
比如一個社區(qū)系統(tǒng)初期一定是只有少量的種子用戶在生產(chǎn)內(nèi)容瞳收,而大部分的用戶都在“圍觀”別人在說什么。此時厢汹,整體流量較小螟深,而寫流量可能只占整體流量的百分之一,那么即使整體的QPS到了1w烫葬,寫請求QPS也只是到了100界弧,如果要對寫請求做性能優(yōu)化,性價比不高搭综。
但隨著業(yè)務發(fā)展垢箕,可能遇到一些存在高并發(fā)寫請求場景,比如秒殺兑巾。假設你的商城策劃了一期秒殺活動条获,活動在第五天的00:00開始,僅限前200名蒋歌,那么秒殺即將開始時帅掘,后臺會顯示用戶正在瘋狂地刷新APP或者瀏覽器來保證自己能夠盡量早的看到商品。
但讀請求依舊過高奋姿,如何應對?
2 優(yōu)化方案
丟棄訂單
最早期素标,量太大扛不住称诗,直接前端隨機reject一些,返回搶單失敗头遭,簡單粗暴寓免,但有效癣诱,比如10萬人搶100個iPhone,只要能提前預測有大概1萬以上的人參與(通過資格確認袜香、報名等方式收集信息)撕予,那么直接請求進來以后隨機擋回去99%的流量都沒有啥問題。
優(yōu)化吞吐
中間有段時間蜈首,提前準備一大批機器实抡,服務化、分庫分表搞定后端性能欢策,讓前端業(yè)務可以加一定量的機器吆寨,然后搞穩(wěn)定性,依賴關系踩寇,容量規(guī)劃啄清,做彈性,提升吞吐量俺孙。
異步隊列
使用可堆積的消息隊列或內(nèi)存消息隊列辣卒。若搶單具有強順序,則先都進隊列睛榄,然后拿前N (就是庫存數(shù))個出來平滑處理荣茫,剩下都可作為失敗進行批處理。
甚至還可做一個定長隊列懈费,再往里寫直接提示失敗计露。隊列把并發(fā)變成串行,從而去掉了分布式鎖憎乙。
內(nèi)存分配
某些業(yè)務可以考慮預熱票罐,提前在每個機器節(jié)點內(nèi)存分配好庫存數(shù),然后直接在內(nèi)存處理庫存數(shù)泞边。
拆分擴展
對于不同類型该押、不同商家、不同來源的商品阵谚,部署不同的前端促銷集群蚕礼,
分散壓力。比如梢什,按每個整點發(fā)起秒殺奠蹬,具體到每個商家,其實量就不大了嗡午。
服務降級
越重要的搶單囤躁,大家越關心自己有沒有搶到,而不是特別在意訂單立即處
理完,也就是說狸演,下單占到位置比處理完成訂單要更有價值言蛇。比如12306春運搶票,只要告訴用戶你搶到了票宵距,但預計1個小時后訂單才會處理完腊尚,用戶有這個明確預期即可。用戶不會立馬使用這張票满哪,也不會在意1min還是1h內(nèi)處理完婿斥。
部分方案會導致銷售不足或超賣:
- 銷售不足可以從搶購里加一些名單補發(fā),也可以加一輪秒殺
- 超賣比較麻煩翩瓜,所以一般會多備一點貨受扳,比如搶100個iPhone,提前準備110 個
因為用戶查詢的是少量的商品數(shù)據(jù)兔跌,屬查詢熱點數(shù)據(jù)勘高,可采用緩存將請求盡量擋在上層緩存,能被靜態(tài)化的數(shù)據(jù)(比如商城里的圖片和視頻數(shù)據(jù))盡量做到靜態(tài)化坟桅,這就可命中CDN節(jié)點緩存华望,減少Web服務器的查詢量和帶寬負擔。Web服務器比如Nginx可以直接訪問分布式緩存節(jié)點仅乓,從而避免請求到達Tomcat等業(yè)務服務器赖舟。
當然,你可以加上一些限流的策略夸楣,比如對短時間之內(nèi)來自某一個用戶宾抓、某一個IP或者某一臺設備的重復請求做丟棄處理。
通過這幾種方式豫喧,請求就可以盡量擋在數(shù)據(jù)庫之外了石洗。
稍微緩解了讀請求之后,00:00分秒殺活動準時開始紧显,用戶瞬間向電商系統(tǒng)請求生成訂單讲衫,扣減庫存,用戶的這些寫操作都是不經(jīng)過緩存直達數(shù)據(jù)庫的孵班。1秒鐘之內(nèi)涉兽,有1萬個數(shù)據(jù)庫連接同時達到,系統(tǒng)的數(shù)據(jù)庫瀕臨崩潰篙程,尋找能夠應對如此高并發(fā)的寫請求方案迫在眉睫枷畏。這時你想到了消息隊列。
理解消息隊列
把消息隊列看作暫時存儲數(shù)據(jù)的一個容器虱饿,它是一個平衡低速系統(tǒng)和高速系統(tǒng)處理任務時間差的工具拥诡。
比如古代臣子朝見皇上陳述國家大事丹允,等皇上決策。但大臣很多袋倔,如果同時去找皇上,皇上肯定會崩潰折柠。后來變成臣子到午門后要原地等皇上將他們一個一個地召見進大殿商議宾娜,這就緩解皇上處理事情的壓力。
可以把午門看作一個暫時容納臣子的容器扇售,即消息隊列:
- 在Java線程池中我們就會使用一個隊列來暫時存儲提交的任務前塔,等待有空閑的線程處理這些任務
- os中斷的下半部分也會使用工作隊列來實現(xiàn)延后執(zhí)行
- 實現(xiàn)一個RPC框架時,也會將從網(wǎng)絡上接收到的請求寫到隊列里承冰,再啟動若干個工作線程來處理
那如何用消息隊列解決秒殺場景下的問題呢华弓?
削去秒殺場景下的峰值寫流量
在秒殺場景下短時間之內(nèi)數(shù)據(jù)庫的寫流量很高,按以前思路困乒,應該分庫分表寂屏。若已做了分庫分表,則需要擴展更多數(shù)據(jù)庫應對更高寫流量娜搂。
但無論是分庫分表還是擴充更多數(shù)據(jù)庫都會很復雜迁霎,因為你需要遷移數(shù)據(jù)庫中的數(shù)據(jù),這個時間就要按天甚至周計算百宇。
而在秒殺場景下高并發(fā)的寫請求并不是持續(xù)的考廉,也不是經(jīng)常發(fā)生,而只有在秒殺活動開始后的幾s或十幾s時間內(nèi)才存在携御。
為了應對這十幾s瞬間寫高峰昌粤,而去花費幾天甚至幾周擴容DB,再在秒殺之后花費幾天做縮容啄刹,得不償失涮坐!
所以思路是:將秒殺請求暫存在MQ,然后業(yè)務服務器會響應用戶“秒殺結果正在計算”鸵膏,釋放了系統(tǒng)資源之后再處理其它用戶請求膊升。
在后臺啟動若干個隊列處理程序消費MQ中的消息,再執(zhí)行校驗庫存谭企、下單等邏輯廓译。因為只有有限個隊列處理線程在執(zhí)行,所以落入后端DB上的并發(fā)請求有限债查。而請求是可以在MQ被短暫堆積非区,當庫存被消耗完后,消息隊列中堆積的請求就可以被丟棄了盹廷。
這就是MQ在秒殺系統(tǒng)中主要作用:削峰填谷征绸,可以削平短暫流量高峰,雖說堆積會造成請求被短暫延遲處理,但只要我們時刻監(jiān)控MQ中的堆積長度管怠,在堆積量超過一定量時淆衷,增加隊列處理機數(shù)量來提升消息處理能力即可,而且秒殺用戶對于短暫延遲知曉秒殺的結果也有一定容忍度渤弛。
注意是“短暫”延遲祝拯,若長時間沒有給用戶公示秒殺結果,則用戶會懷疑秒殺活動有黑幕她肯。所以在使用MQ應對流量峰值時佳头,需要對隊列處理的時間、前端寫入流量的大小晴氨、數(shù)據(jù)庫處理能力做好評估康嘉,然后根據(jù)不同的量級來決定部署多少臺隊列處理程序。
比如你的秒殺商品有1000件籽前,處理一次購買請求的時間是500ms亭珍,那么總共就需要500s的時間。這時你部署10個隊列處理程序枝哄,那么秒殺請求的處理時間就是50s块蚌,也就是說用戶需要等待50s才可以看到秒殺的結果,這是可以接受的膘格。這時會并發(fā)10個請求到達數(shù)據(jù)庫峭范,并不會對數(shù)據(jù)庫造成很大的壓力。
通過異步處理簡化秒殺請求中的業(yè)務流程
其實在大量的寫請求“攻擊”你的電商系統(tǒng)的時候瘪贱,消息隊列除了發(fā)揮主要的削峰填谷的作用之外纱控,還可以實現(xiàn)異步處理來簡化秒殺請求中的業(yè)務流程,提升系統(tǒng)的性能菜秦。
你想甜害,在剛才提到的秒殺場景下,我們在處理購買請求時需要500ms球昨。這時你分析了一下整個的購買流程尔店,發(fā)現(xiàn)這里面會有主要的業(yè)務邏輯,也會有次要的業(yè)務邏輯:比如說主慰,主要的流程是生成訂單嚣州、扣減庫存;次要的流程可能是我們在下單購買成功之后會給用戶發(fā)放優(yōu)惠券共螺,會增加用戶的積分该肴。
假如發(fā)放優(yōu)惠券的耗時是50ms,增加用戶積分的耗時也是50ms藐不,那么如果我們將發(fā)放優(yōu)惠券匀哄、增加積分的操作放在另外一個隊列處理機中執(zhí)行秦效,那么整個流程就縮短到了400ms,性能提升了20%涎嚼,處理這1000件商品的時間就變成了400s阱州。如果我們還是希望能在50s之內(nèi)看到秒殺結果的話,只需要部署8個隊列程序就好了法梯。
經(jīng)過將一些業(yè)務流程異步處理之后贡耽,我們的秒殺系統(tǒng)部署結構也會有所改變:
解耦實現(xiàn)秒殺系統(tǒng)模塊之間松耦合
除了異步處理和削峰填谷以外刀森,消息隊列在秒殺系統(tǒng)中起到的另一個作用是解耦合。
比如數(shù)據(jù)團隊對你說影暴,在秒殺活動之后想要統(tǒng)計活動的數(shù)據(jù)软能,借此來分析活動商品的受歡迎程度、購買者人群的特點以及用戶對于秒殺互動的滿意程度等等指標欺抗。而我們需要將大量的數(shù)據(jù)發(fā)送給數(shù)據(jù)團隊,那么要怎么做呢?
一個思路是:使用HTTP或者RPC的方式來同步地調(diào)用至耻,也就是數(shù)據(jù)團隊這邊提供一個接口,我們實時將秒殺的數(shù)據(jù)推送給它镊叁,但是這樣調(diào)用會有兩個問題:
整體系統(tǒng)的耦合性比較強尘颓,當數(shù)據(jù)團隊的接口發(fā)生故障時,會影響到秒殺系統(tǒng)的可用性晦譬。
當數(shù)據(jù)系統(tǒng)需要新的字段疤苹,就要變更接口的參數(shù),那么秒殺系統(tǒng)也要隨著一起變更敛腌。
這時卧土,我們可以考慮使用消息隊列降低業(yè)務系統(tǒng)和數(shù)據(jù)系統(tǒng)的直接耦合度。
秒殺系統(tǒng)產(chǎn)生一條購買數(shù)據(jù)后像樊,我們可以先把全部數(shù)據(jù)發(fā)送給消息隊列尤莺,然后數(shù)據(jù)團隊再訂閱這個消息隊列的話題,這樣它們就可以接收到數(shù)據(jù)生棍,然后再做過濾和處理了颤霎。
秒殺系統(tǒng)在這樣解耦合之后,數(shù)據(jù)系統(tǒng)的故障就不會影響到秒殺系統(tǒng)了涂滴,同時當數(shù)據(jù)系統(tǒng)需要新的字段時友酱,只需要解析消息隊列中的消息,拿到需要的數(shù)據(jù)就好了柔纵。
異步處理粹污、解耦合和削峰填谷是消息隊列在秒殺系統(tǒng)設計中起到的主要作用,其中異步處理可以簡化業(yè)務流程中的步驟首量,提升系統(tǒng)性能壮吩;削峰填谷可以削去到達秒殺系統(tǒng)的峰值流量进苍,讓業(yè)務邏輯的處理更加緩和;解耦合可以將秒殺系統(tǒng)和數(shù)據(jù)系統(tǒng)解耦開鸭叙,這樣兩個系統(tǒng)的任何變更都不會影響到另一個系統(tǒng)觉啊,
如果你的系統(tǒng)想要提升寫入性能實現(xiàn)系統(tǒng)的低耦合,想要抵擋高并發(fā)的寫流量沈贝,那么你就可以考慮使用消息隊列來完成杠人。
課程小結
本節(jié)課,我結合自己的實際經(jīng)驗宋下,主要帶你了解了消息隊列在高并發(fā)系統(tǒng)設計中起到的作用以及一些注意事項嗡善,你需要了解的重點如下:
削峰填谷是消息隊列最主要的作用,但是會造成請求處理的延遲学歧。
異步處理是提升系統(tǒng)性能的神器罩引,但是你需要分清同步流程和異步流程的邊界,同時消息存在著丟失的風險枝笨,我們需要考慮如何確保消息一定到達袁铐。
解耦合可以提升你的整體系統(tǒng)的魯棒性。
當然横浑,你要知道剔桨,在使用消息隊列之后雖然可以解決現(xiàn)有的問題,但是系統(tǒng)的復雜度也會上升徙融。比如上面提到的業(yè)務流程中洒缀,同步流程和異步流程的邊界在哪里?消息是否會丟失欺冀,是否會重復帝洪?請求的延遲如何能夠減少?消息接收的順序是否會影響到業(yè)務流程的正常執(zhí)行脚猾?如果消息處理流程失敗了之后是否需要補發(fā)葱峡?這些問題都是我們需要考慮的。我會利用接下來的兩節(jié)課針對最主要的兩個問題來講講解決思路:一個是如何處理消息的丟失和重復龙助,另一個是如何減少消息的延遲砰奕。
引入了消息隊列的同時也會引入了新的問題,需要新的方案來解決提鸟,這就是系統(tǒng)設計的挑戰(zhàn)军援,也是系統(tǒng)設計獨有的魅力,而我們也會在這些挑戰(zhàn)中不斷提升技術能力和系統(tǒng)設計能力称勋。