前言
只有光頭才能變強。
文本已收錄至我的GitHub倉庫琢蛤,歡迎Star:https://github.com/ZhongFuCheng3y/3y
公司用到的很多技術(shù)博其,自己之前都沒學(xué)過(尬)慕淡,于是只能慢慢補了沸毁。這次給大家寫寫我學(xué)習(xí)消息隊列的筆記以清,希望對大家有幫助掷倔。
一勒葱、什么是消息隊列?
消息隊列不知道大家看到這個詞的時候死遭,會不會覺得它是一個比較高端的技術(shù)呀潭,反正我是覺得它好像是挺牛逼的钠署。
消息隊列,一般我們會簡稱它為MQ(Message Queue)舰蟆,嗯身害,就是很直白的簡寫题造。
我們先不管消息(Message)這個詞猾瘸,來看看隊列(Queue)牵触。這一看揽思,隊列大家應(yīng)該都熟悉吧钉汗。
隊列是一種先進先出的數(shù)據(jù)結(jié)構(gòu)。
在Java里邊,已經(jīng)實現(xiàn)了不少的隊列了:
那為什么還需要消息隊列(MQ)這種中間件呢肪凛?伟墙?滴铅?其實這個問題汉匙,跟之前我學(xué)Redis的時候很像。Redis是一個以key-value
形式存儲的內(nèi)存數(shù)據(jù)庫守伸,明明我們可以使用類似HashMap這種實現(xiàn)類就可以達到類似的效果了,那還為什么要Redis见芹?《Redis合集》
- 到這里,大家可以先猜猜為什么要用消息隊列(MQ)這種中間件阅懦,下面會繼續(xù)補充耳胎。
消息隊列可以簡單理解為:把要傳輸?shù)臄?shù)據(jù)放在隊列中怕午。
)
科普:
- 把數(shù)據(jù)放到消息隊列叫做生產(chǎn)者
- 從消息隊列里邊取數(shù)據(jù)叫做消費者
二郁惜、為什么要用消息隊列兆蕉?
為什么要用消息隊列虎韵,也就是在問:用了消息隊列有什么好處缸废。我們看看以下的場景
2.1 解耦
現(xiàn)在我有一個系統(tǒng)A呆奕,系統(tǒng)A可以產(chǎn)生一個userId
然后梁钾,現(xiàn)在有系統(tǒng)B和系統(tǒng)C都需要這個userId
去做相關(guān)的操作
寫成偽代碼可能是這樣的:
public class SystemA {
// 系統(tǒng)B和系統(tǒng)C的依賴
SystemB systemB = new SystemB();
SystemC systemC = new SystemC();
// 系統(tǒng)A獨有的數(shù)據(jù)userId
private String userId = "Java3y";
public void doSomething() {
// 系統(tǒng)B和系統(tǒng)C都需要拿著系統(tǒng)A的userId去操作其他的事
systemB.SystemBNeed2do(userId);
systemC.SystemCNeed2do(userId);
}
}
結(jié)構(gòu)圖如下:
ok零酪,一切平安無事度過了幾個天四苇。
某一天月腋,系統(tǒng)B的負(fù)責(zé)人告訴系統(tǒng)A的負(fù)責(zé)人榆骚,現(xiàn)在系統(tǒng)B的SystemBNeed2do(String userId)
這個接口不再使用了妓肢,讓系統(tǒng)A別去調(diào)它了。
于是纲缓,系統(tǒng)A的負(fù)責(zé)人說"好的祝高,那我就不調(diào)用你了褂策。"斤寂,于是就把調(diào)用系統(tǒng)B接口的代碼給刪掉了:
public void doSomething() {
// 系統(tǒng)A不再調(diào)用系統(tǒng)B的接口了
//systemB.SystemBNeed2do(userId);
systemC.SystemCNeed2do(userId);
}
又過了幾天揪惦,系統(tǒng)D的負(fù)責(zé)人接了個需求器腋,也需要用到系統(tǒng)A的userId纫塌,于是就跑去跟系統(tǒng)A的負(fù)責(zé)人說:"老哥措左,我要用到你的userId怎披,你調(diào)一下我的接口吧"
于是系統(tǒng)A說:"沒問題的瓶摆,這就搞"
然后,系統(tǒng)A的代碼如下:
public class SystemA {
// 已經(jīng)不再需要系統(tǒng)B的依賴了
// SystemB systemB = new SystemB();
// 系統(tǒng)C和系統(tǒng)D的依賴
SystemC systemC = new SystemC();
SystemD systemD = new SystemD();
// 系統(tǒng)A獨有的數(shù)據(jù)
private String userId = "Java3y";
public void doSomething() {
// 已經(jīng)不再需要系統(tǒng)B的依賴了
//systemB.SystemBNeed2do(userId);
// 系統(tǒng)C和系統(tǒng)D都需要拿著系統(tǒng)A的userId去操作其他的事
systemC.SystemCNeed2do(userId);
systemD.SystemDNeed2do(userId);
}
}
時間飛逝:
- 又過了幾天书斜,系統(tǒng)E的負(fù)責(zé)人過來了菩佑,告訴系統(tǒng)A,需要userId搓劫。
- 又過了幾天枪向,系統(tǒng)B的負(fù)責(zé)人過來了咧党,告訴系統(tǒng)A傍衡,還是重新掉那個接口吧蛙埂。
- 又過了幾天绣的,系統(tǒng)F的負(fù)責(zé)人過來了屡江,告訴系統(tǒng)A,需要userId惩嘉。
- …...
于是系統(tǒng)A的負(fù)責(zé)人宏怔,每天都被這給騷擾著,改來改去斜脂,改來改去.......
還有另外一個問題帚戳,調(diào)用系統(tǒng)C的時候片任,如果系統(tǒng)C掛了对供,系統(tǒng)A還得想辦法處理产场。如果調(diào)用系統(tǒng)D時舞竿,由于網(wǎng)絡(luò)延遲骗奖,請求超時了执桌,那系統(tǒng)A是反饋fail
還是重試鼻吮?椎木?
最后香椎,系統(tǒng)A的負(fù)責(zé)人,覺得隔一段時間就改來改去畜伐,沒意思馍惹,于是就跑路了。
然后,公司招來一個大佬万矾,大佬經(jīng)過幾天熟悉悼吱,上來就說:將系統(tǒng)A的userId寫到消息隊列中,這樣系統(tǒng)A就不用經(jīng)常改動了良狈。為什么呢后添?下面我們來一起看看:
系統(tǒng)A將userId寫到消息隊列中遇西,系統(tǒng)C和系統(tǒng)D從消息隊列中拿數(shù)據(jù)。這樣有什么好處严嗜?
-
系統(tǒng)A只負(fù)責(zé)把數(shù)據(jù)寫到隊列中粱檀,誰想要或不想要這個數(shù)據(jù)(消息),系統(tǒng)A一點都不關(guān)心漫玄。
- 即便現(xiàn)在系統(tǒng)D不想要userId這個數(shù)據(jù)了茄蚯,系統(tǒng)B又突然想要userId這個數(shù)據(jù)了,都跟系統(tǒng)A無關(guān)称近,系統(tǒng)A一點代碼都不用改第队。
系統(tǒng)D拿userId不再經(jīng)過系統(tǒng)A哮塞,而是從消息隊列里邊拿刨秆。系統(tǒng)D即便掛了或者請求超時,都跟系統(tǒng)A無關(guān)忆畅,只跟消息隊列有關(guān)衡未。
這樣一來,系統(tǒng)A與系統(tǒng)B家凯、C缓醋、D都解耦了。
2.2 異步
我們再來看看下面這種情況:系統(tǒng)A還是直接調(diào)用系統(tǒng)B绊诲、C送粱、D
代碼如下:
public class SystemA {
SystemB systemB = new SystemB();
SystemC systemC = new SystemC();
SystemD systemD = new SystemD();
// 系統(tǒng)A獨有的數(shù)據(jù)
private String userId ;
public void doOrder() {
// 下訂單
userId = this.order();
// 如果下單成功,則安排其他系統(tǒng)做一些事
systemB.SystemBNeed2do(userId);
systemC.SystemCNeed2do(userId);
systemD.SystemDNeed2do(userId);
}
}
假設(shè)系統(tǒng)A運算出userId具體的值需要50ms掂之,調(diào)用系統(tǒng)B的接口需要300ms抗俄,調(diào)用系統(tǒng)C的接口需要300ms,調(diào)用系統(tǒng)D的接口需要300ms世舰。那么這次請求就需要50+300+300+300=950ms
并且我們得知动雹,系統(tǒng)A做的是主要的業(yè)務(wù),而系統(tǒng)B跟压、C胰蝠、D是非主要的業(yè)務(wù)。比如系統(tǒng)A處理的是訂單下單,而系統(tǒng)B是訂單下單成功了茸塞,那發(fā)送一條短信告訴具體的用戶此訂單已成功躲庄,而系統(tǒng)C和系統(tǒng)D也是處理一些小事而已。
那么此時钾虐,為了提高用戶體驗和吞吐量读跷,其實可以異步地調(diào)用系統(tǒng)B、C禾唁、D的接口效览。所以,我們可以弄成是這樣的:
系統(tǒng)A執(zhí)行完了以后荡短,將userId寫到消息隊列中丐枉,然后就直接返回了(至于其他的操作,則異步處理)掘托。
- 本來整個請求需要用950ms(同步)
- 現(xiàn)在將調(diào)用其他系統(tǒng)接口異步化瘦锹,只需要100ms(異步)
(例子可能舉得不太好,但我覺得說明到點子上就行了闪盔,見諒弯院。)
2.3削峰/限流
我們再來一個場景,現(xiàn)在我們每個月要搞一次大促泪掀,大促期間的并發(fā)可能會很高的听绳,比如每秒3000個請求。假設(shè)我們現(xiàn)在有兩臺機器處理請求异赫,并且每臺機器只能每次處理1000個請求椅挣。
那多出來的1000個請求,可能就把我們整個系統(tǒng)給搞崩了...所以塔拳,有一種辦法鼠证,我們可以寫到消息隊列中:
系統(tǒng)B和系統(tǒng)C根據(jù)自己的能夠處理的請求數(shù)去消息隊列中拿數(shù)據(jù)靠抑,這樣即便有每秒有8000個請求量九,那只是把請求放在消息隊列中,去拿消息隊列的消息由系統(tǒng)自己去控制颂碧,這樣就不會把整個系統(tǒng)給搞崩荠列。
三、使用消息隊列有什么問題稚伍?
經(jīng)過我們上面的場景弯予,我們已經(jīng)可以發(fā)現(xiàn),消息隊列能做的事其實還是蠻多的个曙。
說到這里锈嫩,我們先回到文章的開頭受楼,"明明JDK已經(jīng)有不少的隊列實現(xiàn)了,我們還需要消息隊列中間件呢呼寸?"其實很簡單艳汽,JDK實現(xiàn)的隊列種類雖然有很多種,但是都是簡單的內(nèi)存隊列对雪。為什么我說JDK是簡單的內(nèi)存隊列呢河狐?下面我們來看看要實現(xiàn)消息隊列(中間件)可能要考慮什么問題。
3.1高可用
無論是我們使用消息隊列來做解耦瑟捣、異步還是削峰馋艺,消息隊列肯定不能是單機的。試著想一下迈套,如果是單機的消息隊列捐祠,萬一這臺機器掛了,那我們整個系統(tǒng)幾乎就是不可用了桑李。
所以踱蛀,當(dāng)我們項目中使用消息隊列,都是得集群/分布式
的贵白。要做集群/分布式
就必然希望該消息隊列能夠提供現(xiàn)成的支持率拒,而不是自己寫代碼手動去實現(xiàn)。
3.2 數(shù)據(jù)丟失問題
我們將數(shù)據(jù)寫到消息隊列上禁荒,系統(tǒng)B和C還沒來得及取消息隊列的數(shù)據(jù)猬膨,就掛掉了。如果沒有做任何的措施圈浇,我們的數(shù)據(jù)就丟了寥掐。
學(xué)過Redis的都知道,Redis可以將數(shù)據(jù)持久化磁盤上磷蜀,萬一Redis掛了,還能從磁盤從將數(shù)據(jù)恢復(fù)過來百炬。同樣地褐隆,消息隊列中的數(shù)據(jù)也需要存在別的地方,這樣才盡可能減少數(shù)據(jù)的丟失剖踊。
那存在哪呢庶弃?
- 磁盤?
- 數(shù)據(jù)庫德澈?
- Redis歇攻?
- 分布式文件系統(tǒng)?
同步存儲還是異步存儲梆造?
3.3消費者怎么得到消息隊列的數(shù)據(jù)缴守?
消費者怎么從消息隊列里邊得到數(shù)據(jù)?有兩種辦法:
- 生產(chǎn)者將數(shù)據(jù)放到消息隊列中,消息隊列有數(shù)據(jù)了屡穗,主動叫消費者去拿(俗稱push)
- 消費者不斷去輪訓(xùn)消息隊列贴捡,看看有沒有新的數(shù)據(jù),如果有就消費(俗稱pull)
3.4其他
除了這些村砂,我們在使用的時候還得考慮各種的問題:
- 消息重復(fù)消費了怎么辦袄谜?
- 我想保證消息是絕對有順序的怎么做础废?
- ……..
雖然消息隊列給我們帶來了那么多的好處汛骂,但同時我們發(fā)現(xiàn)引入消息隊列也會提高系統(tǒng)的復(fù)雜性。市面上現(xiàn)在已經(jīng)有不少消息隊列輪子了评腺,每種消息隊列都有自己的特點香缺,選取哪種MQ還得好好斟酌。
最后
本文主要講解了什么是消息隊列歇僧,消息隊列可以為我們帶來什么好處图张,以及一個消息隊列可能會涉及到哪些問題。希望給大家?guī)硪欢ǖ膸椭?/p>
參考資料:
- Kafka簡明教程
- 消息隊列使用的四種場景介紹诈悍,有圖有解析祸轮,一看就懂
- 消息隊列設(shè)計精要
- 消息隊列的使用場景是怎樣的
樂于輸出干貨的Java技術(shù)公眾號:Java3y。公眾號內(nèi)有200多篇原創(chuàng)技術(shù)文章侥钳、海量視頻資源适袜、精美腦圖,不妨來關(guān)注一下舷夺!
覺得我的文章寫得不錯苦酱,不妨點一下贊!