消息隊(duì)列就像是一個(gè)暫時(shí)存儲(chǔ)數(shù)據(jù)的一個(gè)容器,是一個(gè)平衡低速系統(tǒng)和高速系統(tǒng)處理任務(wù)時(shí)間差的工具。
一,消息隊(duì)列的作用
1.削峰
如果短時(shí)間之內(nèi)數(shù)據(jù)庫(kù)的寫(xiě)流量很高,那么正常思路是對(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ú)疑是得不償失的。
所以:應(yīng)該將秒殺請(qǐng)求暫存在消息隊(duì)列中,然后業(yè)務(wù)服務(wù)器會(huì)響應(yīng)用戶“秒殺結(jié)果正在計(jì)算中”,釋放了系統(tǒng)資源之后再處理其它用戶的請(qǐng)求。
在后臺(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)求就可以被丟棄了。
2.通過(guò)異步簡(jiǎn)化業(yè)務(wù)流程
把主要的業(yè)務(wù)流程與次要的業(yè)務(wù)流程分開(kāi),放在不同的對(duì)列中,不僅可以簡(jiǎn)化流程還能進(jìn)一步提高系統(tǒng)性能。
3.解耦實(shí)現(xiàn)系統(tǒng)模塊之間松耦合
本系統(tǒng)與其他系統(tǒng)有數(shù)據(jù)同步需求時(shí),有新數(shù)據(jù)產(chǎn)生時(shí),可以先把全部數(shù)據(jù)發(fā)送給消息隊(duì)列,然后其他系統(tǒng)再訂閱這個(gè)消息隊(duì)列的話題,這樣大大降低了系統(tǒng)間耦合度,不至于一個(gè)系統(tǒng)故障或變更影響其他系統(tǒng)。
總的來(lái)說(shuō)
1.削峰填谷是消息隊(duì)列最主要的作用,但是會(huì)造成請(qǐng)求處理的延遲。
2.異步處理是提升系統(tǒng)性能的神器,但是要分清同步流程和異步流程的邊界,同時(shí)消息存在著丟失的風(fēng)險(xiǎn),需要考慮如何確保消息一定到達(dá)。
3.解耦合可以提升整體系統(tǒng)的魯棒性。
二,消息丟失的問(wèn)題
消息丟失的三個(gè)場(chǎng)景:
1.消息從生產(chǎn)者寫(xiě)入到消息隊(duì)列的過(guò)程。
2.消息在消息隊(duì)列中的存儲(chǔ)場(chǎng)景。
3.消息被消費(fèi)者消費(fèi)的過(guò)程。
1. 在消息生產(chǎn)的過(guò)程中丟失消息
消息的生產(chǎn)者——業(yè)務(wù)服務(wù)器與消息隊(duì)列服務(wù)器兩者之間的網(wǎng)絡(luò)發(fā)生抖動(dòng),消息就有可能因?yàn)榫W(wǎng)絡(luò)的錯(cuò)誤而丟失。
針對(duì)這種情況,建議采用的方案是消息重傳來(lái)解決。
不過(guò),這種方案可能會(huì)造成消息的重復(fù),從而導(dǎo)致在消費(fèi)的時(shí)候會(huì)重復(fù)消費(fèi)同樣的消息。
2. 在消息隊(duì)列中丟失消息
拿 Kafka 舉例,消息在 Kafka 中是存儲(chǔ)在本地磁盤(pán)上的,而為了減少消息存儲(chǔ)時(shí)對(duì)磁盤(pán)的隨機(jī) I/O,一般會(huì)將消息先寫(xiě)入到操作系統(tǒng)的 Page Cache 中,然后再找合適的時(shí)機(jī)刷新到磁盤(pán)上。Kafka 可以配置當(dāng)達(dá)到某一時(shí)間間隔,或者累積一定的消息數(shù)量的時(shí)候再刷盤(pán),也就是所說(shuō)的異步刷盤(pán)。如果發(fā)生機(jī)器掉電或者機(jī)器異常重啟,那么 Page Cache 中還沒(méi)有來(lái)得及刷盤(pán)的消息就會(huì)丟失了。
3. 消費(fèi)的過(guò)程中也存在消息丟失的可能
一個(gè)消費(fèi)者消費(fèi)消息的進(jìn)度是記錄在消息隊(duì)列集群中的,而消費(fèi)的過(guò)程分為三步:接收消息、處理消息、更新消費(fèi)進(jìn)度。
這里面接收消息和處理消息的過(guò)程都可能會(huì)發(fā)生異?;蛘呤?,比如說(shuō),消息接收時(shí)網(wǎng)絡(luò)發(fā)生抖動(dòng),導(dǎo)致消息并沒(méi)有被正確的接收到;處理消息時(shí)可能發(fā)生一些業(yè)務(wù)的異常導(dǎo)致處理流程未執(zhí)行完成,這時(shí)如果更新消費(fèi)進(jìn)度,那么這條失敗的消息就永遠(yuǎn)不會(huì)被處理了,也可以認(rèn)為是丟失了。
三,如何只消費(fèi)一次
顯然,為了避免消息丟失,我們需要付出兩方面的代價(jià):一方面是性能的損耗;一方面可能造成消息重復(fù)消費(fèi)。
那么如何保證消息只被消費(fèi)一次?
想要完全的避免消息重復(fù)的發(fā)生是很難做到的,因?yàn)榫W(wǎng)絡(luò)的抖動(dòng)、機(jī)器的宕機(jī)和處理的異常都是比較難以避免的,因此只要保證即使消費(fèi)到了重復(fù)的消息,從最終結(jié)果來(lái)看和只消費(fèi)一次是等同的就好了,也就是保證在消息的生產(chǎn)和消費(fèi)的過(guò)程是“冪等”(一件事兒無(wú)論做多少次都和做一次產(chǎn)生的結(jié)果是一樣的)的。
唯一ID的方式
實(shí)現(xiàn)冪等有一種較好的方法是為每一個(gè)消息生成一個(gè)唯一的 ID,然后在使用這個(gè)消息的時(shí)候,先比對(duì)這個(gè) ID 是否已經(jīng)存在,如果存在,則認(rèn)為消息已經(jīng)被使用過(guò)。
樂(lè)觀鎖的方式
除此之外,還可以在業(yè)務(wù)層面來(lái)保證消息只消費(fèi)一次,比如通過(guò)樂(lè)觀鎖的方式來(lái)解決。
具體的操作方式是這樣的:在數(shù)據(jù)中增加一個(gè)版本號(hào)的字段,在生產(chǎn)消息時(shí)先查詢這條數(shù)據(jù)的版本號(hào),并且將版本號(hào)連同消息一起發(fā)送給消息隊(duì)列。消費(fèi)端在拿到消息和版本號(hào)后,在執(zhí)行消費(fèi)的程序的時(shí)候帶上版本號(hào),如果版本號(hào)已經(jīng)被更新了則無(wú)法消費(fèi)成功。
四,關(guān)于消費(fèi)的性能
1.我們可以使用消息隊(duì)列提供的工具,或者通過(guò)發(fā)送監(jiān)控消息的方式,來(lái)監(jiān)控消息的延遲情況;
2.橫向擴(kuò)展消費(fèi)者是提升消費(fèi)處理能力的重要方式;
3.選擇高性能的數(shù)據(jù)存儲(chǔ)方式,配合零拷貝技術(shù),可以提升消息的消費(fèi)性能。