1鹏浅、為什么使用消息隊列
如果有人問你這個問題,期望的一個回答是說剂桥,你們公司有個什么業(yè)務(wù)場景,這個業(yè)務(wù)場景有個什么技術(shù)挑戰(zhàn)属提,如果不用 MQ 可能會很麻煩权逗,但是現(xiàn)在用了 MQ 之后帶給了你很多的好處。
消息隊列的使用場景有很多冤议,但是歸根結(jié)底都可以用六字真言來概括:解耦斟薇、異步、削峰恕酸。
1.1 解耦
假設(shè)現(xiàn)在有一個系統(tǒng)A堪滨,可以產(chǎn)生userId,系統(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 = "winner";
public void doSomething() {
// 系統(tǒng)B和系統(tǒng)C都需要拿著系統(tǒng)A的userId去操作其他的事
systemB.SystemBNeed2do(userId);
systemC.SystemCNeed2do(userId);
}
}
系統(tǒng)上線后袱箱,平穩(wěn)運行了一段時間,一切貌似很完美义矛。
某一天发笔,系統(tǒng)B的負責人告訴系統(tǒng)A的負責人,現(xiàn)在系統(tǒng)B的SystemBNeed2do(String userId)
這個接口不再使用了凉翻,讓系統(tǒng)A別去調(diào)它了了讨。
于是,系統(tǒng)A的負責人說”好的制轰,那我就不調(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的負責人接了個需求垃杖,也需要用到系統(tǒng)A的userId男杈,于是就跑去跟系統(tǒng)A的負責人說:“老哥,我要用到你的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的負責人過來了侧巨,告訴系統(tǒng)A舅锄,需要userId。
- 又過了幾天司忱,系統(tǒng)B的負責人過來了皇忿,告訴系統(tǒng)A,還是重新掉那個接口吧坦仍。
- 又過了幾天鳍烁,系統(tǒng)F的負責人過來了,告訴系統(tǒng)A繁扎,需要userId幔荒。
- …...
于是系統(tǒng)A的負責人,每天都被這給騷擾著梳玫,改來改去爹梁,改來改去.......
還有另外一個問題,調(diào)用系統(tǒng)C的時候提澎,如果系統(tǒng)C掛了姚垃,系統(tǒng)A還得想辦法處理。如果調(diào)用系統(tǒng)D時盼忌,由于網(wǎng)絡(luò)延遲积糯,請求超時了,那系統(tǒng)A是反饋失敗還是重試谦纱?
最后看成,系統(tǒng)A的負責人,覺得隔一段時間就改來改去服协,沒意思绍昂,于是就跑路了……跑路了……了
然后,公司招來一個大佬偿荷,大佬經(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只負責把數(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都解耦了乳讥。
1.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(異步)
1.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)給搞崩趁窃。
2牧挣、副作用
上文列舉了消息隊列的三個用武之地,表面來看醒陆,很多難題只要引入了消息隊列就能夠化腐朽為神奇瀑构。但是,事分兩面刨摩,下面就要說說消息隊列的副作用寺晌。
2.1 高可用問題
系統(tǒng)引入的外部依賴越多,越容易掛掉澡刹。本來你就是 A 系統(tǒng)調(diào)用 BCD 三個系統(tǒng)的接口就好了呻征,人家 ABCD 四個系統(tǒng)好好的,沒啥問題罢浇,你偏加個 MQ 進來陆赋,萬一 MQ 掛了,導致整套系統(tǒng)崩潰的嚷闭,你不就完了攒岛?
為了保證消息隊列的高可用性,一般都會采用集群/分布式的部署方式胞锰。
RabbitMQ 的鏡像集群模式可以保證每個queue存在于多個實例上阵子,每次寫消息到queue的時候,都會自動把消息同步到多個實例胜蛉。
而kafka本來就是天然的分布式消息隊列挠进,由多個broker構(gòu)成集群色乾,一個topic可以劃分為多個partition,每個partition可以存在于不同的broker上领突,每個partition就放一部分數(shù)據(jù)暖璧。也就是說一個topic的數(shù)據(jù),是分散放在多個機器上的君旦,每個機器就放一部分數(shù)據(jù)澎办。
2.2 一致性問題
A 系統(tǒng)處理完了直接返回成功了,調(diào)用方就以為請求就成功了金砍。但是問題是局蚀,假如 BCD 三個系統(tǒng)那里,BD 兩個系統(tǒng)寫庫成功了恕稠,結(jié)果 C 系統(tǒng)寫庫失敗了怎么辦琅绅?會導致數(shù)據(jù)的一致性被破壞。
2.3 復(fù)雜性問題
消息隊列實際是一種非常復(fù)雜的架構(gòu)鹅巍,引入它有很多好處千扶,但是也得針對它帶來的壞處做各種額外的技術(shù)方案和架構(gòu)來規(guī)避掉,比如下面的“奪命三板斧”:
- 如何保證消息不被重復(fù)消費骆捧?
- 如何保證消息不丟失澎羞?
- 如何保證消息的順序投遞?
3敛苇、常見中間件對比
- ActiveMQ 的社區(qū)算是比較成熟妆绞,但是較目前來說,ActiveMQ 的性能比較差枫攀,而且版本迭代很慢摆碉,不推薦使用。
- RabbitMQ 在吞吐量方面雖然稍遜于 Kafka 和 RocketMQ 脓豪,但是由于它基于 erlang 開發(fā)巷帝,所以并發(fā)能力很強,性能極其好扫夜,延時很低楞泼,達到微秒級。但是也因為 RabbitMQ 基于 erlang 開發(fā)笤闯,所以國內(nèi)很少有公司有實力做erlang源碼級別的研究和定制堕阔。如果業(yè)務(wù)場景對并發(fā)量要求不是太高(十萬級、百萬級)颗味,那這四種消息隊列中超陆,RabbitMQ 一定是你的首選。如
- RocketMQ 阿里出品,Java 系開源項目时呀,源代碼我們可以直接閱讀张漂,然后可以定制自己公司的MQ,并且 RocketMQ 有阿里巴巴的實際業(yè)務(wù)場景的實戰(zhàn)考驗谨娜。RocketMQ 社區(qū)活躍度相對較為一般航攒,不過也還可以,文檔相對來說簡單一些趴梢,然后接口這塊不是按照標準 JMS 規(guī)范走的有些系統(tǒng)要遷移需要修改大量代碼漠畜。
- kafka 的特點其實很明顯,就是僅僅提供較少的核心功能坞靶,但是提供超高的吞吐量憔狞,ms 級的延遲,極高的可用性以及可靠性彰阴,而且分布式可以任意擴展瘾敢。同時 kafka 最好是支撐較少的 topic 數(shù)量即可,保證其超高吞吐量硝枉。kafka 唯一的一點劣勢是有可能消息重復(fù)消費,那么對數(shù)據(jù)準確性會造成極其輕微的影響倦微,在大數(shù)據(jù)領(lǐng)域中以及日志采集中妻味,這點輕微影響可以忽略這個特性天然適合大數(shù)據(jù)實時計算以及日志收集。