在課程一開(kāi)始,我就帶你了解了高并發(fā)系統(tǒng)設(shè)計(jì)的三個(gè)目標(biāo):性能、可用性和可擴(kuò)展性,而在提升系統(tǒng)性能方面我們一直關(guān)注的是系統(tǒng)的查詢性能,也用了很多的篇幅去講解數(shù)據(jù)庫(kù)的分布式改造,各類(lèi)緩存的原理和使用技巧。究其原因在于我們遇到的大部分場(chǎng)景都是讀多寫(xiě)少,尤其是在一個(gè)系統(tǒng)的初級(jí)階段。
比如一個(gè)社區(qū)的系統(tǒng)初期一定是只有少量的種子用戶在生產(chǎn)內(nèi)容,而大部分的用戶都在“圍觀”別人在說(shuō)什么。此時(shí),整體的流量比較小,而寫(xiě)流量可能只占整體流量的百分之一,那么即使整體的 QPS 到了 10000 次 / 秒,寫(xiě)請(qǐng)求也只是到了每秒 100 次,如果要對(duì)寫(xiě)請(qǐng)求做性能優(yōu)化,它的性價(jià)比確實(shí)不太高。
但隨著業(yè)務(wù)發(fā)展,你可能會(huì)遇到一些存在高并發(fā)寫(xiě)請(qǐng)求的場(chǎng)景,其中秒殺搶購(gòu)就是最典型的場(chǎng)景。假設(shè)你的商城策劃了一期秒殺活動(dòng),活動(dòng)在第五天的 00:00 開(kāi)始,僅限前 200 名,那么秒殺即將開(kāi)始時(shí),后臺(tái)會(huì)顯示用戶正在瘋狂地刷新 APP 或者瀏覽器來(lái)保證自己能夠盡量早的看到商品。
這時(shí),你面對(duì)的依舊是讀請(qǐng)求過(guò)高,那么應(yīng)對(duì)的措施有哪些呢?
因?yàn)橛脩舨樵兊氖巧倭康纳唐窋?shù)據(jù),屬于查詢的熱點(diǎn)數(shù)據(jù),你可以采用緩存策略將請(qǐng)求盡量擋在上層的緩存中,能被靜態(tài)化的數(shù)據(jù)(比如商城里的圖片和視頻數(shù)據(jù))盡量做到靜態(tài)化,這樣就可以命中 CDN 節(jié)點(diǎn)緩存減少 Web 服務(wù)器的查詢量和帶寬負(fù)擔(dān)。Web 服務(wù)器比如 Nginx 可以直接訪問(wèn)分布式緩存節(jié)點(diǎn),從而避免請(qǐng)求到達(dá) Tomcat 等業(yè)務(wù)服務(wù)器。
當(dāng)然,你可以加上一些限流的策略,比如對(duì)短時(shí)間之內(nèi)來(lái)自某一個(gè)用戶、某一個(gè) IP 或者某一臺(tái)設(shè)備的重復(fù)請(qǐng)求做丟棄處理。
通過(guò)這幾種方式,請(qǐng)求就可以盡量擋在數(shù)據(jù)庫(kù)之外了。
稍微緩解了讀請(qǐng)求之后,00:00 分秒殺活動(dòng)準(zhǔn)時(shí)開(kāi)始,用戶瞬間向電商系統(tǒng)請(qǐng)求生成訂單,扣減庫(kù)存,用戶的這些寫(xiě)操作都是不經(jīng)過(guò)緩存直達(dá)數(shù)據(jù)庫(kù)的。1 秒鐘之內(nèi),有 1 萬(wàn)個(gè)數(shù)據(jù)庫(kù)連接同時(shí)達(dá)到,系統(tǒng)的數(shù)據(jù)庫(kù)瀕臨崩潰,尋找能夠應(yīng)對(duì)如此高并發(fā)的寫(xiě)請(qǐng)求方案迫在眉睫。這時(shí)你想到了消息隊(duì)列。
我所理解的消息隊(duì)列
你應(yīng)該已經(jīng)了解消息隊(duì)列到底是什么了,所以我不再講解它的概念,只聊聊自己對(duì)消息隊(duì)列的看法。我在歷年的工作經(jīng)歷中,一直把消息隊(duì)列看作暫時(shí)存儲(chǔ)數(shù)據(jù)的一個(gè)容器,認(rèn)為它是一個(gè)平衡低速系統(tǒng)和高速系統(tǒng)處理任務(wù)時(shí)間差的工具,我給你舉個(gè)形象的例子。
比如古代的臣子經(jīng)常去朝見(jiàn)皇上陳述一些國(guó)家大事,等著皇上拍板做決策。但是大臣很多,如果同時(shí)去找皇上,你說(shuō)一句我說(shuō)一句,皇上肯定會(huì)崩潰。后來(lái)變成臣子到了午門(mén)之后要原地等著皇上將他們一個(gè)一個(gè)地召見(jiàn)進(jìn)大殿商議國(guó)事,這樣就可以緩解皇上處理事情的壓力了。你可以把午門(mén)看作一個(gè)暫時(shí)容納臣子的容器,也就是我們所說(shuō)的消息隊(duì)列。
其實(shí)你在一些組件中都會(huì)看到消息隊(duì)列的影子:
- 在 Java 線程池中我們就會(huì)使用一個(gè)隊(duì)列來(lái)暫時(shí)存儲(chǔ)提交的任務(wù),等待有空閑的線程處理這些任務(wù);
- 操作系統(tǒng)中,中斷的下半部分也會(huì)使用工作隊(duì)列來(lái)實(shí)現(xiàn)延后執(zhí)行;
- 我們?cè)趯?shí)現(xiàn)一個(gè) RPC 框架時(shí),也會(huì)將從網(wǎng)絡(luò)上接收到的請(qǐng)求寫(xiě)到隊(duì)列里,再啟動(dòng)若干個(gè)工作線程來(lái)處理。
總之,隊(duì)列是在系統(tǒng)設(shè)計(jì)時(shí)一種常見(jiàn)的組件。
那么我們?nèi)绾斡孟㈥?duì)列解決秒殺場(chǎng)景下的問(wèn)題呢?接下來(lái),我們結(jié)合具體的例子來(lái)看看消息隊(duì)列在秒殺場(chǎng)景下起到的作用。
削去秒殺場(chǎng)景下的峰值寫(xiě)流量
剛才提到,在秒殺場(chǎng)景下短時(shí)間之內(nèi)數(shù)據(jù)庫(kù)的寫(xiě)流量會(huì)很高,那么依照我們以前的思路應(yīng)該對(duì)數(shù)據(jù)做分庫(kù)分表。如果已經(jīng)做了分庫(kù)分表,那么就需要擴(kuò)展更多的數(shù)據(jù)庫(kù)來(lái)應(yīng)對(duì)更高的寫(xiě)流量。但是無(wú)論是分庫(kù)分表還是擴(kuò)充更多的數(shù)據(jù)庫(kù)都會(huì)比較復(fù)雜,原因是你需要將數(shù)據(jù)庫(kù)中的數(shù)據(jù)做遷移,這個(gè)時(shí)間就要按天甚至按周來(lái)計(jì)算了。
而在秒殺場(chǎng)景下高并發(fā)的寫(xiě)請(qǐng)求并不是持續(xù)的,也不是經(jīng)常發(fā)生的,而只有在秒殺活動(dòng)開(kāi)始后的幾秒或者十幾秒時(shí)間內(nèi)才會(huì)存在。為了應(yīng)對(duì)這十幾秒的瞬間寫(xiě)高峰花費(fèi)幾天甚至幾周的時(shí)間來(lái)擴(kuò)容數(shù)據(jù)庫(kù),再在秒殺之后花費(fèi)幾天的時(shí)間來(lái)做縮容,這無(wú)疑是得不償失的。
所以我們的思路是:將秒殺請(qǐng)求暫存在消息隊(duì)列中,然后業(yè)務(wù)服務(wù)器會(huì)響應(yīng)用戶“秒殺結(jié)果正在計(jì)算中”,釋放了系統(tǒng)資源之后再處理其它用戶的請(qǐng)求。
我們會(huì)在后臺(tái)啟動(dòng)若干個(gè)隊(duì)列處理程序消費(fèi)消息隊(duì)列中的消息,再執(zhí)行校驗(yàn)庫(kù)存、下單等邏輯。因?yàn)橹挥杏邢迋€(gè)隊(duì)列處理線程在執(zhí)行,所以落入后端數(shù)據(jù)庫(kù)上的并發(fā)請(qǐng)求是有限的。而請(qǐng)求是可以在消息隊(duì)列中被短暫地堆積,當(dāng)庫(kù)存被消耗完之后,消息隊(duì)列中堆積的請(qǐng)求就可以被丟棄了。

這就是消息隊(duì)列在秒殺系統(tǒng)中最主要的作用:削峰填谷,也就是說(shuō)它可以削平短暫的流量高峰,雖說(shuō)堆積會(huì)造成請(qǐng)求被短暫延遲處理,但是只要我們時(shí)刻監(jiān)控消息隊(duì)列中的堆積長(zhǎng)度,在堆積量超過(guò)一定量時(shí),增加隊(duì)列處理機(jī)數(shù)量來(lái)提升消息的處理能力就好了,而且秒殺的用戶對(duì)于短暫延遲知曉秒殺的結(jié)果也是有一定容忍度的。
這里需要注意一下,我所說(shuō)的是“短暫”延遲,如果長(zhǎng)時(shí)間沒(méi)有給用戶公示秒殺結(jié)果,那么用戶可能就會(huì)懷疑你的秒殺活動(dòng)有貓膩了。所以在使用消息隊(duì)列應(yīng)對(duì)流量峰值時(shí),需要對(duì)隊(duì)列處理的時(shí)間、前端寫(xiě)入流量的大小、數(shù)據(jù)庫(kù)處理能力做好評(píng)估,然后根據(jù)不同的量級(jí)來(lái)決定部署多少臺(tái)隊(duì)列處理程序。
比如你的秒殺商品有 1000 件,處理一次購(gòu)買(mǎi)請(qǐng)求的時(shí)間是 500ms,那么總共就需要 500s 的時(shí)間。這時(shí)你部署 10 個(gè)隊(duì)列處理程序,那么秒殺請(qǐng)求的處理時(shí)間就是 50s,也就是說(shuō)用戶需要等待 50s 才可以看到秒殺的結(jié)果,這是可以接受的。這時(shí)會(huì)并發(fā) 10 個(gè)請(qǐng)求到達(dá)數(shù)據(jù)庫(kù),并不會(huì)對(duì)數(shù)據(jù)庫(kù)造成很大的壓力。
通過(guò)異步處理簡(jiǎn)化秒殺請(qǐng)求中的業(yè)務(wù)流程
其實(shí)在大量的寫(xiě)請(qǐng)求“攻擊”你的電商系統(tǒng)的時(shí)候,消息隊(duì)列除了發(fā)揮主要的削峰填谷的作用之外,還可以實(shí)現(xiàn)異步處理來(lái)簡(jiǎn)化秒殺請(qǐng)求中的業(yè)務(wù)流程,提升系統(tǒng)的性能。
你想,在剛才提到的秒殺場(chǎng)景下,我們?cè)谔幚碣?gòu)買(mǎi)請(qǐng)求時(shí)需要 500ms。這時(shí)你分析了一下整個(gè)的購(gòu)買(mǎi)流程,發(fā)現(xiàn)這里面會(huì)有主要的業(yè)務(wù)邏輯,也會(huì)有次要的業(yè)務(wù)邏輯:比如說(shuō),主要的流程是生成訂單、扣減庫(kù)存;次要的流程可能是我們?cè)谙聠钨?gòu)買(mǎi)成功之后會(huì)給用戶發(fā)放優(yōu)惠券,會(huì)增加用戶的積分。
假如發(fā)放優(yōu)惠券的耗時(shí)是 50ms,增加用戶積分的耗時(shí)也是 50ms,那么如果我們將發(fā)放優(yōu)惠券、增加積分的操作放在另外一個(gè)隊(duì)列處理機(jī)中執(zhí)行,那么整個(gè)流程就縮短到了 400ms,性能提升了 20%,處理這 1000 件商品的時(shí)間就變成了 400s。如果我們還是希望能在 50s 之內(nèi)看到秒殺結(jié)果的話,只需要部署 8 個(gè)隊(duì)列程序就好了。 經(jīng)過(guò)將一些業(yè)務(wù)流程異步處理之后,我們的秒殺系統(tǒng)部署結(jié)構(gòu)也會(huì)有所改變:

解耦實(shí)現(xiàn)秒殺系統(tǒng)模塊之間松耦合
除了異步處理和削峰填谷以外,消息隊(duì)列在秒殺系統(tǒng)中起到的另一個(gè)作用是解耦合。
比如數(shù)據(jù)團(tuán)隊(duì)對(duì)你說(shuō),在秒殺活動(dòng)之后想要統(tǒng)計(jì)活動(dòng)的數(shù)據(jù),借此來(lái)分析活動(dòng)商品的受歡迎程度、購(gòu)買(mǎi)者人群的特點(diǎn)以及用戶對(duì)于秒殺互動(dòng)的滿意程度等等指標(biāo)。而我們需要將大量的數(shù)據(jù)發(fā)送給數(shù)據(jù)團(tuán)隊(duì),那么要怎么做呢?
一個(gè)思路是:使用 HTTP 或者 RPC 的方式來(lái)同步地調(diào)用,也就是數(shù)據(jù)團(tuán)隊(duì)這邊提供一個(gè)接口,我們實(shí)時(shí)將秒殺的數(shù)據(jù)推送給它,但是這樣調(diào)用會(huì)有兩個(gè)問(wèn)題:
1、整體系統(tǒng)的耦合性比較強(qiáng),當(dāng)數(shù)據(jù)團(tuán)隊(duì)的接口發(fā)生故障時(shí),會(huì)影響到秒殺系統(tǒng)的可用性。
2、當(dāng)數(shù)據(jù)系統(tǒng)需要新的字段,就要變更接口的參數(shù),那么秒殺系統(tǒng)也要隨著一起變更。
這時(shí),我們可以考慮使用消息隊(duì)列降低業(yè)務(wù)系統(tǒng)和數(shù)據(jù)系統(tǒng)的直接耦合度。
秒殺系統(tǒng)產(chǎn)生一條購(gòu)買(mǎi)數(shù)據(jù)后,我們可以先把全部數(shù)據(jù)發(fā)送給消息隊(duì)列,然后數(shù)據(jù)團(tuán)隊(duì)再訂閱這個(gè)消息隊(duì)列的話題,這樣它們就可以接收到數(shù)據(jù),然后再做過(guò)濾和處理了。
秒殺系統(tǒng)在這樣解耦合之后,數(shù)據(jù)系統(tǒng)的故障就不會(huì)影響到秒殺系統(tǒng)了,同時(shí)當(dāng)數(shù)據(jù)系統(tǒng)需要新的字段時(shí),只需要解析消息隊(duì)列中的消息,拿到需要的數(shù)據(jù)就好了。

異步處理、解耦合和削峰填谷是消息隊(duì)列在秒殺系統(tǒng)設(shè)計(jì)中起到的主要作用,其中異步處理可以簡(jiǎn)化業(yè)務(wù)流程中的步驟,提升系統(tǒng)性能;削峰填谷可以削去到達(dá)秒殺系統(tǒng)的峰值流量,讓業(yè)務(wù)邏輯的處理更加緩和;解耦合可以將秒殺系統(tǒng)和數(shù)據(jù)系統(tǒng)解耦開(kāi),這樣兩個(gè)系統(tǒng)的任何變更都不會(huì)影響到另一個(gè)系統(tǒng),
如果你的系統(tǒng)想要提升寫(xiě)入性能實(shí)現(xiàn)系統(tǒng)的低耦合,想要抵擋高并發(fā)的寫(xiě)流量,那么你就可以考慮使用消息隊(duì)列來(lái)完成。
總結(jié)
1、削峰填谷是消息隊(duì)列的主要作用
2、分離業(yè)務(wù)異步處理更能增加大系統(tǒng)處理能力
3、解耦可以提升系統(tǒng)的魯棒性