消息隊列介紹與設(shè)計


消息隊列已經(jīng)逐漸成為企業(yè)IT系統(tǒng)內(nèi)部通信的核心手段胖齐。它具有低耦合玻淑、可靠投遞、廣播呀伙、流量控制补履、最終一致性等一系列功能,成為異步RPC的主要手段之一剿另。

老司機簡介

王燁箫锤,現(xiàn)在是美團旅游后臺研發(fā)組的程序猿,之前曾經(jīng)在百度雨女、去哪和優(yōu)酷工作過麻汰,專注Java后臺開發(fā)。對于網(wǎng)絡(luò)編程和并發(fā)編程具有濃厚的興趣戚篙,曾經(jīng)做過一些基礎(chǔ)組件,也翻過一些源碼溺职,屬于比較典型的宅男技術(shù)控岔擂。期待能夠與更多知己,在coding的路上并肩前行~

當今市面上有很多主流的消息中間件浪耘,如老牌的ActiveMQ乱灵、RabbitMQ,炙手可熱的Kafka七冲,阿里巴巴自主開發(fā)的Notify痛倚、MetaQ、RocketMQ等澜躺。本文不會一一介紹這些消息隊列的所有特性蝉稳,而是探討一下自主開發(fā)設(shè)計一個消息隊列時抒蚜,你需要思考和設(shè)計的重要方面。過程中我們會參考這些成熟消息隊列的很多重要思想耘戚。

本文首先會闡述什么時候你需要一個消息隊列嗡髓,然后以Push模型為主,從零開始分析設(shè)計一個消息隊列時需要考慮到的問題收津,如RPC饿这、高可用、順序和重復(fù)消息撞秋、可靠投遞长捧、消費關(guān)系解析等。

也會分析以Kafka為代表的pull模型所具備的優(yōu)點吻贿。最后是一些高級主題串结,如用批量/異步提高性能、pull模型的系統(tǒng)設(shè)計理念廓八、存儲子系統(tǒng)的設(shè)計奉芦、流量控制的設(shè)計、公平調(diào)度的實現(xiàn)等剧蹂。其中最后四個方面會放在下篇講解声功。

何時需要消息隊列

當你需要使用消息隊列時,首先需要考慮它的必要性宠叼∠劝停可以使用mq的場景有很多,最常用的幾種冒冬,是做業(yè)務(wù)解耦/最終一致性/廣播/錯峰流控等伸蚯。反之,如果需要強一致性简烤,關(guān)注業(yè)務(wù)邏輯的處理結(jié)果剂邮,則RPC顯得更為合適。

解耦

解耦是消息隊列要解決的最本質(zhì)問題横侦。所謂解耦挥萌,簡單點講就是一個事務(wù),只關(guān)心核心的流程枉侧。而需要依賴其他系統(tǒng)但不那么重要的事情引瀑,有通知即可,無需等待結(jié)果榨馁。換句話說憨栽,基于消息的模型,關(guān)心的是“通知”,而非“處理”屑柔。

比如在美團旅游屡萤,我們有一個產(chǎn)品中心,產(chǎn)品中心上游對接的是主站锯蛀、移動后臺灭衷、旅游供應(yīng)鏈等各個數(shù)據(jù)源;下游對接的是篩選系統(tǒng)旁涤、API系統(tǒng)等展示系統(tǒng)翔曲。當上游的數(shù)據(jù)發(fā)生變更的時候,如果不使用消息系統(tǒng)劈愚,勢必要調(diào)用我們的接口來更新數(shù)據(jù)瞳遍,就特別依賴產(chǎn)品中心接口的穩(wěn)定性和處理能力。

但其實菌羽,作為旅游的產(chǎn)品中心掠械,也許只有對于旅游自建供應(yīng)鏈,產(chǎn)品中心更新成功才是他們關(guān)心的事情注祖。而對于團購等外部系統(tǒng)猾蒂,產(chǎn)品中心更新成功也好、失敗也罷是晨,并不是他們的職責所在肚菠。他們只需要保證在信息變更的時候通知到我們就好了。

而我們的下游罩缴,可能有更新索引蚊逢、刷新緩存等一系列需求。對于產(chǎn)品中心來說箫章,這也不是我們的職責所在烙荷。說白了,如果他們定時來拉取數(shù)據(jù)檬寂,也能保證數(shù)據(jù)的更新终抽,只是實時性沒有那么強。但使用接口方式去更新他們的數(shù)據(jù)桶至,顯然對于產(chǎn)品中心來說太過于“重量級”了拿诸,只需要發(fā)布一個產(chǎn)品ID變更的通知,由下游系統(tǒng)來處理塞茅,可能更為合理。

再舉一個例子季率,對于我們的訂單系統(tǒng)野瘦,訂單最終支付成功之后可能需要給用戶發(fā)送短信積分什么的,但其實這已經(jīng)不是我們系統(tǒng)的核心流程了。如果外部系統(tǒng)速度偏慢(比如短信網(wǎng)關(guān)速度不好)鞭光,那么主流程的時間會加長很多吏廉,用戶肯定不希望點擊支付過好幾分鐘才看到結(jié)果。那么我們只需要通知短信系統(tǒng)“我們支付成功了”惰许,不一定非要等待它處理完成席覆。

最終一致性

最終一致性指的是兩個系統(tǒng)的狀態(tài)保持一致,要么都成功汹买,要么都失敗佩伤。當然有個時間限制,理論上越快越好晦毙,但實際上在各種異常的情況下生巡,可能會有一定延遲達到最終一致狀態(tài),但最后兩個系統(tǒng)的狀態(tài)是一樣的见妒。

業(yè)界有一些為“最終一致性”而生的消息隊列孤荣,如Notify(阿里)、QMQ(去哪兒)等须揣,其設(shè)計初衷盐股,就是為了交易系統(tǒng)中的高可靠通知。

以一個銀行的轉(zhuǎn)賬過程來理解最終一致性耻卡,轉(zhuǎn)賬的需求很簡單疯汁,如果A系統(tǒng)扣錢成功,則B系統(tǒng)加錢一定成功劲赠。反之則一起回滾涛目,像什么都沒發(fā)生一樣。

然而凛澎,這個過程中存在很多可能的意外:

A扣錢成功霹肝,調(diào)用B加錢接口失敗。

A扣錢成功塑煎,調(diào)用B加錢接口雖然成功沫换,但獲取最終結(jié)果時網(wǎng)絡(luò)異常引起超時。

A扣錢成功最铁,B加錢失敗讯赏,A想回滾扣的錢,但A機器down機冷尉。

可見漱挎,想把這件看似簡單的事真正做成,真的不那么容易雀哨。所有跨VM的一致性問題磕谅,從技術(shù)的角度講通用的解決方案是:

強一致性私爷,分布式事務(wù),但落地太難且成本太高膊夹,后文會具體提到衬浑。

最終一致性,主要是用“記錄”和“補償”的方式放刨。在做所有的不確定的事情之前工秩,先把事情記錄下來,然后去做不確定的事情进统,結(jié)果可能是:成功助币、失敗或是不確定,“不確定”(例如超時等)可以等價為失敗麻昼。成功就可以把記錄的東西清理掉了奠支,對于失敗和不確定,可以依靠定時任務(wù)等方式把所有失敗的事情重新搞一遍抚芦,直到成功為止倍谜。

回到剛才的例子,系統(tǒng)在A扣錢成功的情況下叉抡,把要給B“通知”這件事記錄在庫里(為了保證最高的可靠性可以把通知B系統(tǒng)加錢和扣錢成功這兩件事維護在一個本地事務(wù)里)尔崔,通知成功則刪除這條記錄,通知失敗或不確定則依靠定時任務(wù)補償性地通知我們褥民,直到我們把狀態(tài)更新成正確的為止季春。

整個這個模型依然可以基于RPC來做,但可以抽象成一個統(tǒng)一的模型消返,基于消息隊列來做一個“企業(yè)總線”载弄。

具體來說,本地事務(wù)維護業(yè)務(wù)變化和通知消息撵颊,一起落地(失敗則一起回滾)宇攻,然后RPC到達broker,在broker成功落地后倡勇,RPC返回成功逞刷,本地消息可以刪除。否則本地消息一直靠定時任務(wù)輪詢不斷重發(fā)妻熊,這樣就保證了消息可靠落地broker夸浅。

broker往consumer發(fā)送消息的過程類似,一直發(fā)送消息扔役,直到consumer發(fā)送消費成功確認帆喇。我們先不理會重復(fù)消息的問題,通過兩次消息落地加補償亿胸,下游是一定可以收到消息的坯钦。然后依賴狀態(tài)機版本號等方式做判重法严,更新自己的業(yè)務(wù),就實現(xiàn)了最終一致性葫笼。

最終一致性不是消息隊列的必備特性活合,但確實可以依靠消息隊列來做最終一致性的事情旧巾。另外模庐,所有不保證100%不丟消息的消息隊列烘豌,理論上無法實現(xiàn)最終一致性铅乡。好吧秸妥,應(yīng)該說理論上的100%豪诲,排除系統(tǒng)嚴重故障和bug蒿讥。

像Kafka一類的設(shè)計挥等,在設(shè)計層面上就有丟消息的可能(比如定時刷盤友绝,如果掉電就會丟消息)。哪怕只丟千分之一的消息肝劲,業(yè)務(wù)也必須用其他的手段來保證結(jié)果正確迁客。

廣播

消息隊列的基本功能之一是進行廣播。如果沒有消息隊列辞槐,每當一個新的業(yè)務(wù)方接入掷漱,我們都要聯(lián)調(diào)一次新接口。有了消息隊列榄檬,我們只需要關(guān)心消息是否送達了隊列卜范,至于誰希望訂閱,是下游的事情鹿榜,無疑極大地減少了開發(fā)和聯(lián)調(diào)的工作量海雪。

比如本文開始提到的產(chǎn)品中心發(fā)布產(chǎn)品變更的消息,以及景點庫很多去重更新的消息舱殿,可能“關(guān)心”方有很多個奥裸,但產(chǎn)品中心和景點庫只需要發(fā)布變更消息即可,誰關(guān)心誰接入怀薛。

錯峰與流控

試想上下游對于事情的處理能力是不同的刺彩。比如,Web前端每秒承受上千萬的請求枝恋,并不是什么神奇的事情创倔,只需要加多一點機器,再搭建一些LVS負載均衡設(shè)備和Nginx等即可焚碌。但數(shù)據(jù)庫的處理能力卻十分有限畦攘,即使使用SSD加分庫分表,單機的處理能力仍然在萬級十电。由于成本的考慮知押,我們不能奢求數(shù)據(jù)庫的機器數(shù)量追上前端叹螟。

這種問題同樣存在于系統(tǒng)和系統(tǒng)之間,如短信系統(tǒng)可能由于短板效應(yīng)台盯,速度卡在網(wǎng)關(guān)上(每秒幾百次請求)罢绽,跟前端的并發(fā)量不是一個數(shù)量級。但用戶晚上個半分鐘左右收到短信静盅,一般是不會有太大問題的良价。如果沒有消息隊列,兩個系統(tǒng)之間通過協(xié)商蒿叠、滑動窗口等復(fù)雜的方案也不是說不能實現(xiàn)明垢。

但系統(tǒng)復(fù)雜性指數(shù)級增長,勢必在上游或者下游做存儲市咽,并且要處理定時痊银、擁塞等一系列問題。而且每當有處理能力有差距的時候施绎,都需要單獨開發(fā)一套邏輯來維護這套邏輯溯革。所以,利用中間系統(tǒng)轉(zhuǎn)儲兩個系統(tǒng)的通信內(nèi)容粘姜,并在下游系統(tǒng)有能力處理這些消息的時候鬓照,再處理這些消息,是一套相對較通用的方式孤紧。

總而言之豺裆,消息隊列不是萬能的。對于需要強事務(wù)保證而且延遲敏感的号显,RPC是優(yōu)于消息隊列的臭猜。

對于一些無關(guān)痛癢,或者對于別人非常重要但是對于自己不是那么關(guān)心的事情押蚤,可以利用消息隊列去做蔑歌。

支持最終一致性的消息隊列,能夠用來處理延遲不那么敏感的“分布式事務(wù)”場景揽碘,而且相對于笨重的分布式事務(wù)次屠,可能是更優(yōu)的處理方式。

當上下游系統(tǒng)處理能力存在差距的時候雳刺,利用消息隊列做一個通用的“漏斗”劫灶。在下游有能力處理的時候,再進行分發(fā)掖桦。

如果下游有很多系統(tǒng)關(guān)心你的系統(tǒng)發(fā)出的通知的時候本昏,果斷地使用消息隊列吧。

如何設(shè)計一個消息隊列

綜述

我們現(xiàn)在明確了消息隊列的使用場景枪汪,下一步就是如何設(shè)計實現(xiàn)一個消息隊列了涌穆。

基于消息的系統(tǒng)模型怔昨,不一定需要broker(消息隊列服務(wù)端)。市面上的的Akka(actor模型)宿稀、ZeroMQ等趁舀,其實都是基于消息的系統(tǒng)設(shè)計范式,但是沒有broker祝沸。

我們之所以要設(shè)計一個消息隊列赫编,并且配備broker,無外乎要做兩件事情:

消息的轉(zhuǎn)儲奋隶,在更合適的時間點投遞,或者通過一系列手段輔助消息最終能送達消費機悦荒。

規(guī)范一種范式和通用的模式唯欣,以滿足解耦、最終一致性搬味、錯峰等需求境氢。

掰開了揉碎了看,最簡單的消息隊列可以做成一個消息轉(zhuǎn)發(fā)器碰纬,把一次RPC做成兩次RPC萍聊。發(fā)送者把消息投遞到服務(wù)端(以下簡稱broker),服務(wù)端再將消息轉(zhuǎn)發(fā)一手到接收端悦析,就是這么簡單寿桨。

一般來講,設(shè)計消息隊列的整體思路是先build一個整體的數(shù)據(jù)流,例如producer發(fā)送給broker,broker發(fā)送給consumer,consumer回復(fù)消費確認强戴,broker刪除/備份消息等亭螟。

利用RPC將數(shù)據(jù)流串起來。然后考慮RPC的高可用性骑歹,盡量做到無狀態(tài)预烙,方便水平擴展。

之后考慮如何承載消息堆積道媚,然后在合適的時機投遞消息扁掸,而處理堆積的最佳方式,就是存儲最域,存儲的選型需要綜合考慮性能/可靠性和開發(fā)維護成本等諸多因素谴分。

為了實現(xiàn)廣播功能,我們必須要維護消費關(guān)系羡宙,可以利用zk/config server等保存消費關(guān)系狸剃。

在完成了上述幾個功能后,消息隊列基本就實現(xiàn)了狗热。然后我們可以考慮一些高級特性钞馁,如可靠投遞虑省,事務(wù)特性,性能優(yōu)化等僧凰。

下面我們會以設(shè)計消息隊列時重點考慮的模塊為主線探颈,穿插灌輸一些消息隊列的特性實現(xiàn)方法,來具體分析設(shè)計實現(xiàn)一個消息隊列時的方方面面训措。

實現(xiàn)隊列基本功能

RPC通信協(xié)議

剛才講到伪节,所謂消息隊列,無外乎兩次RPC加一次轉(zhuǎn)儲绩鸣,當然需要消費端最終做消費確認的情況是三次RPC怀大。既然是RPC,就必然牽扯出一系列話題呀闻,什么負載均衡啊化借、服務(wù)發(fā)現(xiàn)啊、通信協(xié)議啊捡多、序列化協(xié)議啊蓖康,等等。在這一塊垒手,我的強烈建議是不要重復(fù)造輪子蒜焊。

利用公司現(xiàn)有的RPC框架:Thrift也好,Dubbo也好科贬,或者是其他自定義的框架也好泳梆。因為消息隊列的RPC,和普通的RPC沒有本質(zhì)區(qū)別榜掌。當然了鸭丛,自主利用Memchached或者Redis協(xié)議重新寫一套RPC框架并非不可(如MetaQ使用了自己封裝的Gecko NIO框架,卡夫卡也用了類似的協(xié)議)唐责。但實現(xiàn)成本和難度無疑倍增鳞溉。排除對效率的極端要求,都可以使用現(xiàn)成的RPC框架鼠哥。

簡單來講熟菲,服務(wù)端提供兩個RPC服務(wù),一個用來接收消息朴恳,一個用來確認消息收到抄罕。并且做到不管哪個server收到消息和確認消息,結(jié)果一致即可于颖。當然這中間可能還涉及跨IDC的服務(wù)的問題呆贿。這里和RPC的原則是一致的,盡量優(yōu)先選擇本機房投遞。你可能會問做入,如果producer和consumer本身就在兩個機房了冒晰,怎么辦?首先竟块,broker必須保證感知的到所有consumer的存在壶运。其次,producer盡量選擇就近的機房就好了浪秘。

高可用

其實所有的高可用蒋情,是依賴于RPC和存儲的高可用來做的。先來看RPC的高可用耸携,美團的基于MTThrift的RPC框架棵癣,阿里的Dubbo等,其本身就具有服務(wù)自動發(fā)現(xiàn)夺衍,負載均衡等功能浙巫。而消息隊列的高可用,只要保證broker接受消息和確認消息的接口是冪等的刷后,并且consumer的幾臺機器處理消息是冪等的,這樣就把消息隊列的可用性渊抄,轉(zhuǎn)交給RPC框架來處理了尝胆。

那么怎么保證冪等呢?最簡單的方式莫過于共享存儲护桦。broker多機器共享一個DB或者一個分布式文件/kv系統(tǒng)含衔,則處理消息自然是冪等的。就算有單點故障二庵,其他節(jié)點可以立刻頂上贪染。另外failover可以依賴定時任務(wù)的補償,這是消息隊列本身天然就可以支持的功能催享。存儲系統(tǒng)本身的可用性我們不需要操太多心杭隙,放心大膽的交給DBA們吧!

對于不共享存儲的隊列因妙,如Kafka使用分區(qū)加主備模式痰憎,就略微麻煩一些。需要保證每一個分區(qū)內(nèi)的高可用性攀涵,也就是每一個分區(qū)至少要有一個主備且需要做數(shù)據(jù)的同步铣耘,關(guān)于這塊HA的細節(jié),可以參考下篇pull模型消息系統(tǒng)設(shè)計以故。

服務(wù)端承載消息堆積的能力

消息到達服務(wù)端如果不經(jīng)過任何處理就到接收者了蜗细,broker就失去了它的意義。為了滿足我們錯峰/流控/最終可達等一系列需求怒详,把消息存儲下來炉媒,然后選擇時機投遞就顯得是順理成章的了踪区。

只是這個存儲可以做成很多方式。比如存儲在內(nèi)存里橱野,存儲在分布式KV里朽缴,存儲在磁盤里,存儲在數(shù)據(jù)庫里等等水援。但歸結(jié)起來密强,主要有持久化和非持久化兩種。

持久化的形式能更大程度地保證消息的可靠性(如斷電等不可抗外力)蜗元,并且理論上能承載更大限度的消息堆積(外存的空間遠大于內(nèi)存)或渤。

但并不是每種消息都需要持久化存儲。很多消息對于投遞性能的要求大于可靠性的要求奕扣,且數(shù)量極大(如日志)薪鹦。這時候,消息不落地直接暫存內(nèi)存惯豆,嘗試幾次failover池磁,最終投遞出去也未嘗不可。

市面上的消息隊列普遍兩種形式都支持楷兽。當然具體的場景還要具體結(jié)合公司的業(yè)務(wù)來看地熄。

存儲子系統(tǒng)的選擇

我們來看看如果需要數(shù)據(jù)落地的情況下各種存儲子系統(tǒng)的選擇。理論上芯杀,從速度來看端考,文件系統(tǒng)>分布式KV(持久化)>分布式文件系統(tǒng)>數(shù)據(jù)庫,而可靠性卻截然相反揭厚。還是要從支持的業(yè)務(wù)場景出發(fā)作出最合理的選擇却特,如果你們的消息隊列是用來支持支付/交易等對可靠性要求非常高,但對性能和量的要求沒有這么高筛圆,而且沒有時間精力專門做文件存儲系統(tǒng)的研究裂明,DB是最好的選擇。

但是DB受制于IOPS太援,如果要求單broker 5位數(shù)以上的QPS性能漾岳,基于文件的存儲是比較好的解決方案。整體上可以采用數(shù)據(jù)文件+索引文件的方式處理粉寞,具體這塊的設(shè)計比較復(fù)雜尼荆,可以參考下篇的存儲子系統(tǒng)設(shè)計。

分布式KV(如MongoDB唧垦,HBase)等捅儒,或者持久化的Redis,由于其編程接口較友好,性能也比較可觀巧还,如果在可靠性要求不是那么高的場景鞭莽,也不失為一個不錯的選擇。

消費關(guān)系解析

現(xiàn)在我們的消息隊列初步具備了轉(zhuǎn)儲消息的能力麸祷。下面一個重要的事情就是解析發(fā)送接收關(guān)系澎怒,進行正確的消息投遞了。

市面上的消息隊列定義了一堆讓人暈頭轉(zhuǎn)向的名詞阶牍,如JMS 規(guī)范中的Topic/Queue喷面,Kafka里面的Topic/Partition/ConsumerGroup,RabbitMQ里面的Exchange等等走孽。拋開現(xiàn)象看本質(zhì)惧辈,無外乎是單播與廣播的區(qū)別。所謂單播磕瓷,就是點到點盒齿;而廣播,是一點對多點困食。當然边翁,對于互聯(lián)網(wǎng)的大部分應(yīng)用來說,組間廣播硕盹、組內(nèi)單播是最常見的情形符匾。

消息需要通知到多個業(yè)務(wù)集群,而一個業(yè)務(wù)集群內(nèi)有很多臺機器莱睁,只要一臺機器消費這個消息就可以了。

當然這不是絕對的芒澜,很多時候組內(nèi)的廣播也是有適用場景的仰剿,如本地緩存的更新等等。另外痴晦,消費關(guān)系除了組內(nèi)組間南吮,可能會有多級樹狀關(guān)系。這種情況太過于復(fù)雜誊酌,一般不列入考慮范圍部凑。所以,一般比較通用的設(shè)計是支持組間廣播碧浊,不同的組注冊不同的訂閱涂邀。組內(nèi)的不同機器,如果注冊一個相同的ID箱锐,則單播比勉;如果注冊不同的ID(如IP地址+端口),則廣播。

至于廣播關(guān)系的維護浩聋,一般由于消息隊列本身都是集群观蜗,所以都維護在公共存儲上,如config server衣洁、zookeeper等墓捻。維護廣播關(guān)系所要做的事情基本是一致的:

發(fā)送關(guān)系的維護。

發(fā)送關(guān)系變更時的通知坊夫。

隊列高級特性設(shè)計

上面都是些消息隊列基本功能的實現(xiàn)砖第,下面來看一些關(guān)于消息隊列特性相關(guān)的內(nèi)容,不管可靠投遞/消息丟失與重復(fù)以及事務(wù)乃至于性能践樱,不是每個消息隊列都會照顧到厂画,所以要依照業(yè)務(wù)的需求,來仔細衡量各種特性實現(xiàn)的成本拷邢,利弊袱院,最終做出最為合理的設(shè)計。

可靠投遞(最終一致性)

這是個激動人心的話題瞭稼,完全不丟消息忽洛,究竟可不可能?答案是环肘,完全可能欲虚,前提是消息可能會重復(fù),并且悔雹,在異常情況下复哆,要接受消息的延遲。

方案說簡單也簡單腌零,就是每當要發(fā)生不可靠的事情(RPC等)之前梯找,先將消息落地,然后發(fā)送益涧。當失敗或者不知道成功失斝獯浮(比如超時)時,消息狀態(tài)是待發(fā)送闲询,定時任務(wù)不停輪詢所有待發(fā)送消息久免,最終一定可以送達。

具體來說:

producer往broker發(fā)送消息之前扭弧,需要做一次落地阎姥。

請求到server后,server確保數(shù)據(jù)落地后再告訴客戶端發(fā)送成功鸽捻。

支持廣播的消息隊列需要對每個待發(fā)送的endpoint丁寄,持久化一個發(fā)送狀態(tài)氨淌,直到所有endpoint狀態(tài)都OK才可刪除消息。

對于各種不確定(超時伊磺、down機盛正、消息沒有送達、送達后數(shù)據(jù)沒落地屑埋、數(shù)據(jù)落地了回復(fù)沒收到)豪筝,其實對于發(fā)送方來說,都是一件事情摘能,就是消息沒有送達续崖。

重推消息所面臨的問題就是消息重復(fù)。重復(fù)和丟失就像兩個噩夢团搞,你必須要面對一個严望。好在消息重復(fù)還有處理的機會,消息丟失再想找回就難了逻恐。

Anyway像吻,作為一個成熟的消息隊列,應(yīng)該盡量在各個環(huán)節(jié)減少重復(fù)投遞的可能性复隆,不能因為重復(fù)有解決方案就放縱的亂投遞拨匆。

最后說一句,不是所有的系統(tǒng)都要求最終一致性或者可靠投遞挽拂,比如一個論壇系統(tǒng)惭每、一個招聘系統(tǒng)。一個重復(fù)的簡歷或話題被發(fā)布亏栈,可能比丟失了一個發(fā)布顯得更讓用戶無法接受台腥。不斷重復(fù)一句話,任何基礎(chǔ)組件要服務(wù)于業(yè)務(wù)場景绒北。

消費確認

當broker把消息投遞給消費者后黎侈,消費者可以立即響應(yīng)我收到了這個消息。但收到了這個消息只是第一步镇饮,我能不能處理這個消息卻不一定蜓竹』福或許因為消費能力的問題储藐,系統(tǒng)的負荷已經(jīng)不能處理這個消息;或者是剛才狀態(tài)機里面提到的消息不是我想要接收的消息嘶是,主動要求重發(fā)钙勃。

把消息的送達和消息的處理分開,這樣才真正的實現(xiàn)了消息隊列的本質(zhì)-解耦聂喇。所以辖源,允許消費者主動進行消費確認是必要的蔚携。當然,對于沒有特殊邏輯的消息克饶,默認Auto Ack也是可以的酝蜒,但一定要允許消費方主動ack。

對于正確消費ack的矾湃,沒什么特殊的亡脑。但是對于reject和error,需要特別說明邀跃。reject這件事情霉咨,往往業(yè)務(wù)方是無法感知到的,系統(tǒng)的流量和健康狀況的評估拍屑,以及處理能力的評估是一件非常復(fù)雜的事情途戒。舉個極端的例子,收到一個消息開始build索引僵驰,可能這個消息要處理半個小時喷斋,但消息量卻是非常的小。所以reject這塊建議做成滑動窗口/線程池類似的模型來控制矢渊,消費能力不匹配的時候继准,直接拒絕,過一段時間重發(fā)矮男,減少業(yè)務(wù)的負擔移必。

但業(yè)務(wù)出錯這件事情是只有業(yè)務(wù)方自己知道的,就像上文提到的狀態(tài)機等等毡鉴。這時應(yīng)該允許業(yè)務(wù)方主動ack error,并可以與broker約定下次投遞的時間憎瘸。

重復(fù)消息和順序消息

上文談到重復(fù)消息是不可能100%避免的陈瘦,除非可以允許丟失幌甘,那么痊项,順序消息能否100%滿足呢? 答案是可以,但條件更為苛刻:

允許消息丟失鞍泉。

從發(fā)送方到服務(wù)方到接受者都是單點單線程皱埠。

所以絕對的順序消息基本上是不能實現(xiàn)的,當然在METAQ/Kafka等pull模型的消息隊列中边器,單線程生產(chǎn)/消費忘巧,排除消息丟失,也是一種順序消息的解決方案仗处。

一般來講枣宫,一個主流消息隊列的設(shè)計范式里也颤,應(yīng)該是不丟消息的前提下翅娶,盡量減少重復(fù)消息,不保證消息的投遞順序燥翅。

談到重復(fù)消息森书,主要是兩個話題:

如何鑒別消息重復(fù)谎势,并冪等的處理重復(fù)消息脏榆。

一個消息隊列如何盡量減少重復(fù)消息的投遞须喂。

先來看看第一個話題,每一個消息應(yīng)該有它的唯一身份仔役。不管是業(yè)務(wù)方自定義的骂因,還是根據(jù)IP/PID/時間戳生成的MessageId赃泡,如果有地方記錄這個MessageId升熊,消息到來是能夠進行比對就

能完成重復(fù)的鑒定级野。數(shù)據(jù)庫的唯一鍵/bloom filter/分布式KV中的key蓖柔,都是不錯的選擇。由于消息不能被永久存儲牢贸,所以理論上都存在消息從持久化存儲移除的瞬間上游還在投遞的可能(上游因種種原因投遞失敗潜索,不停重試竹习,都到了下游清理消息的時間)列牺。這種事情都是異常情況下才會發(fā)生的瞎领,畢竟是小眾情況默刚。兩分鐘消息都還沒送達,多送一次又能怎樣呢澜搅?冪等的處理消息是一門藝術(shù)勉躺,因為種種原因重復(fù)消息或者錯亂的消息還是來到了饵溅,說兩種通用的解決方案:

版本號妇萄。

狀態(tài)機。

版本號

舉個簡單的例子幸乒,一個產(chǎn)品的狀態(tài)有上線/下線狀態(tài)罕扎。如果消息1是下線腔召,消息2是上線扮惦。不巧消息1判重失敗,被投遞了兩次馏颂,且第二次發(fā)生在2之后,如果不做重復(fù)性判斷沙绝,顯然最終狀態(tài)是錯誤的鼠锈。

但是购笆,如果每個消息自帶一個版本號同欠。上游發(fā)送的時候铺遂,標記消息1版本號是1,消息2版本號是2撤逢。如果再發(fā)送下線消息蚊荣,則版本號標記為3妇押。下游對于每次消息的處理敲霍,同時維護一個版本號丁存。

每次只接受比當前版本號大的消息解寝。初始版本為0聋伦,當消息1到達時觉增,將版本號更新為1逾礁。消息2到來時嘹履,因為版本號>1.可以接收,同時更新版本號為2.當另一條下線消息到來時幼苛,如果版本號是3.則是真實的下線消息蚓峦。如果是1暑椰,則是重復(fù)投遞的消息一汽。

如果業(yè)務(wù)方只關(guān)心消息重復(fù)不重復(fù)召夹,那么問題就已經(jīng)解決了监憎。但很多時候另一個頭疼的問題來了,就是消息順序如果和想象的順序不一致偷霉。比如應(yīng)該的順序是12类少,到來的順序是21硫狞。則最后會發(fā)生狀態(tài)錯誤残吩。

參考TCP/IP協(xié)議世剖,如果想讓亂序的消息最后能夠正確的被組織旁瘫,那么就應(yīng)該只接收比當前版本號大一的消息酬凳。并且在一個session周期內(nèi)要一直保存各個消息的版本號遭庶。

如果到來的順序是21峦睡,則先把2存起來榨了,待2到來后龙屉,再處理1,這樣重復(fù)性和順序性要求就都達到了唆垃。

狀態(tài)機

基于版本號來處理重復(fù)和順序消息聽起來是個不錯的主意辕万,但凡事總有瑕疵渐尿。使用版本號的最大問題是:

對發(fā)送方必須要求消息帶業(yè)務(wù)版本號涡戳。

下游必須存儲消息的版本號,對于要嚴格保證順序的推正。

還不能只存儲最新的版本號的消息植榕,要把亂序到來的消息都存儲起來尊残。而且必須要對此做出處理寝衫。試想一個永不過期的"session"拐邪,比如一個物品的狀態(tài)扎阶,會不停流轉(zhuǎn)于上下線东臀。那么中間環(huán)節(jié)的所有存儲就必須保留,直到在某個版本號之前的版本一個不丟的到來贱勃,成本太高贵扰。

就剛才的場景看戚绕,如果消息沒有版本號舞丛,該怎么解決呢球切?業(yè)務(wù)方只需要自己維護一個狀態(tài)機吨凑,定義各種狀態(tài)的流轉(zhuǎn)關(guān)系鸵钝。例如恩商,"下線"狀態(tài)只允許接收"上線"消息揽乱,“上線”狀態(tài)只能接收“下線消息”凰棉,如果上線收到上線消息嚷炉,或者下線收到下線消息申屹,在消息不丟失和上游業(yè)務(wù)正確的前提下嚷那。要么是消息發(fā)重了魏宽,要么是順序到達反了。這時消費者只需要把“我不能處理這個消息”告訴投遞者,要求投遞者過一段時間重發(fā)即可蚌斩。而且重發(fā)一定要有次數(shù)限制送膳,比如5次叠聋,避免死循環(huán)碌补,就解決了脑慧。

舉例子說明闷袒,假設(shè)產(chǎn)品本身狀態(tài)是下線囊骤,1是上線消息也物,2是下線消息滑蚯,3是上線消息告材,正常情況下斥赋,消息應(yīng)該的到來順序是123滑绒,但實際情況下收到的消息狀態(tài)變成了3123疑故。

那么下游收到3消息的時候焰扳,判斷狀態(tài)機流轉(zhuǎn)是下線->上線吨悍,可以接收消息育瓜。然后收到消息1,發(fā)現(xiàn)是上線->上線焰手,拒絕接收书妻,要求重發(fā)躲履。然后收到消息2工猜,狀態(tài)是上線->下線,于是接收這個消息魏身。

此時無論重發(fā)的消息1或者3到來叠骑,還是可以接收宙枷。另外的重發(fā)慰丛,在一定次數(shù)拒絕后停止重發(fā)诅病,業(yè)務(wù)正確蝇棉。

中間件對于重復(fù)消息的處理

回歸到消息隊列的話題來講篡殷。上述通用的版本號/狀態(tài)機/ID判重解決方案里板辽,哪些是消息隊列該做的棘催、哪些是消息隊列不該做業(yè)務(wù)方處理的呢劲弦?其實這里沒有一個完全嚴格的定義,但回到我們的出發(fā)點醇坝,我們保證不丟失消息的情況下盡量少重復(fù)消息邑跪,消費順序不保證。那么重復(fù)消息下和亂序消息下業(yè)務(wù)的正確纲仍,應(yīng)該是由消費方保證的呀袱,我們要做的是減少消息發(fā)送的重復(fù)郑叠。

我們無法定義業(yè)務(wù)方的業(yè)務(wù)版本號/狀態(tài)機,如果API里強制需要指定版本號明棍,則顯得過于綁架客戶了乡革。況且,在消費方維護這么多狀態(tài)摊腋,就涉及到一個消費方的消息落地/多機間的同步消費狀態(tài)問題沸版,復(fù)雜度指數(shù)級上升,而且只能解決部分問題兴蒸。

減少重復(fù)消息的關(guān)鍵步驟:

broker記錄MessageId视粮,直到投遞成功后清除,重復(fù)的ID到來不做處理橙凳,這樣只要發(fā)送者在清除周期內(nèi)能夠感知到消息投遞成功蕾殴,就基本不會在server端產(chǎn)生重復(fù)消息笑撞。

對于server投遞到consumer的消息,由于不確定對端是在處理過程中還是消息發(fā)送丟失的情況下钓觉,有必要記錄下投遞的IP地址茴肥。決定重發(fā)之前詢問這個IP,消息處理成功了嗎荡灾?如果詢問無果瓤狐,再重發(fā)。

事務(wù)

持久性是事務(wù)的一個特性批幌,然而只滿足持久性卻不一定能滿足事務(wù)的特性础锐。還是拿扣錢/加錢的例子講。滿足事務(wù)的一致性特征荧缘,則必須要么都不進行郁稍,要么都能成功。

解決方案從大方向上有兩種:

兩階段提交胜宇,分布式事務(wù)耀怜。

本地事務(wù),本地落地桐愉,補償發(fā)送财破。

分布式事務(wù)存在的最大問題是成本太高,兩階段提交協(xié)議从诲,對于仲裁down機或者單點故障左痢,幾乎是一個無解的黑洞。對于交易密集型或者I/O密集型的應(yīng)用系洛,沒有辦法承受這么高的網(wǎng)絡(luò)延遲俊性,系統(tǒng)復(fù)雜性。

并且成熟的分布式事務(wù)一定構(gòu)建與比較靠譜的商用DB和商用中間件上描扯,成本也太高定页。

那如何使用本地事務(wù)解決分布式事務(wù)的問題呢?以本地和業(yè)務(wù)在一個數(shù)據(jù)庫實例中建表為例子绽诚,與扣錢的業(yè)務(wù)操作同一個事務(wù)里典徊,將消息插入本地數(shù)據(jù)庫。如果消息入庫失敗恩够,則業(yè)務(wù)回滾卒落;如果消息入庫成功,事務(wù)提交蜂桶。

然后發(fā)送消息(注意這里可以實時發(fā)送儡毕,不需要等定時任務(wù)檢出,以提高消息實時性)扑媚。以后的問題就是前文的最終一致性問題所提到的了腰湾,只要消息沒有發(fā)送成功贾费,就一直靠定時任務(wù)重試。

這里有一個關(guān)鍵的點檐盟,本地事務(wù)做的褂萧,是業(yè)務(wù)落地和消息落地的事務(wù),而不是業(yè)務(wù)落地和RPC成功的事務(wù)葵萎。這里很多人容易混淆导犹,如果是后者,無疑是事務(wù)嵌套RPC羡忘,是大忌谎痢,會有長事務(wù)死鎖等各種風(fēng)險。

而消息只要成功落地卷雕,很大程度上就沒有丟失的風(fēng)險(磁盤物理損壞除外)节猿。而消息只要投遞到服務(wù)端確認后本地才做刪除,就完成了producer->broker的可靠投遞漫雕,并且當消息存儲異常時滨嘱,業(yè)務(wù)也是可以回滾的。

本地事務(wù)存在兩個最大的使用障礙:

配置較為復(fù)雜浸间,“綁架”業(yè)務(wù)方太雨,必須本地數(shù)據(jù)庫實例提供一個庫表。

對于消息延遲高敏感的業(yè)務(wù)不適用魁蒜。

話說回來囊扳,不是每個業(yè)務(wù)都需要強事務(wù)的《悼矗扣錢和加錢需要事務(wù)保證锥咸,但下單和生成短信卻不需要事務(wù),不能因為要求發(fā)短信的消息存儲投遞失敗而要求下單業(yè)務(wù)回滾细移。所以搏予,一個完整的消息隊列應(yīng)該定義清楚自己可以投遞的消息類型,如事務(wù)型消息葫哗,本地非持久型消息缔刹,以及服務(wù)端不落地的非可靠消息等。

對不同的業(yè)務(wù)場景做不同的選擇劣针。另外事務(wù)的使用應(yīng)該盡量低成本、透明化亿扁,可以依托于現(xiàn)有的成熟框架捺典,如Spring的聲明式事務(wù)做擴展。業(yè)務(wù)方只需要使用@Transactional標簽即可从祝。

性能相關(guān)

異步/同步

首先澄清一個概念襟己,異步引谜,同步和oneway是三件事。異步擎浴,歸根結(jié)底你還是需要關(guān)心結(jié)果的员咽,但可能不是當時的時間點關(guān)心,可以用輪詢或者回調(diào)等方式處理結(jié)果贮预;同步是需要當時關(guān)心的結(jié)果的贝室;而oneway是發(fā)出去就不管死活的方式,這種對于某些完全對可靠性沒有要求的場景還是適用的仿吞,但不是我們重點討論的范疇滑频。

回歸來看,任何的RPC都是存在客戶端異步與服務(wù)端異步的唤冈,而且是可以任意組合的:客戶端同步對服務(wù)端異步峡迷,客戶端異步對服務(wù)端異步,客戶端同步對服務(wù)端同步你虹,客戶端異步對服務(wù)端同步绘搞。

對于客戶端來說,同步與異步主要是拿到一個Result傅物,還是Future(Listenable)的區(qū)別看杭。實現(xiàn)方式可以是線程池,NIO或者其他事件機制挟伙,這里先不展開講楼雹。

服務(wù)端異步可能稍微難理解一點,這個是需要RPC協(xié)議支持的尖阔。參考servlet 3.0規(guī)范贮缅,服務(wù)端可以吐一個future給客戶端,并且在future done的時候通知客戶端介却。

整個過程可以參考下面的代碼:

客戶端同步服務(wù)端異步谴供。

Future future = request(server);//server立刻返回future

synchronized(future){

while(!future.isDone()){

future.wait();//server處理結(jié)束后會notify這個future,并修改isdone標志

}

}

return future.get();

客戶端同步服務(wù)端同步齿坷。

Result result = request(server);

客戶端異步服務(wù)端同步(這里用線程池的方式)桂肌。

Future future = executor.submit(new Callable(){public void call(){

result = request(server);

}})

return future;

客戶端異步服務(wù)端異步。

Future future = request(server);//server立刻返回future

return future

上面說了這么多永淌,其實是想讓大家脫離兩個誤區(qū):

RPC只有客戶端能做異步崎场,服務(wù)端不能。

異步只能通過線程池遂蛀。

那么谭跨,服務(wù)端使用異步最大的好處是什么呢?說到底,是解放了線程和I/O螃宙。試想服務(wù)端有一堆I/O等待處理蛮瞄,如果每個請求都需要同步響應(yīng),每條消息都需要結(jié)果立刻返回谆扎,那么就幾乎沒法做I/O合并(當然接口可以設(shè)計成batch的挂捅,但可能batch發(fā)過來的仍然數(shù)量較少)。

而如果用異步的方式返回給客戶端future堂湖,就可以有機會進行I/O的合并闲先,把幾個批次發(fā)過來的消息一起落地(這種合并對于MySQL等允許batch insert的數(shù)據(jù)庫效果尤其明顯),并且徹底釋放了線程苗缩。不至于說來多少請求開多少線程饵蒂,能夠支持的并發(fā)量直線提高。

來看第二個誤區(qū)酱讶,返回future的方式不一定只有線程池退盯。換句話說,可以在線程池里面進行同步操作泻肯,也可以進行異步操作渊迁,也可以不使用線程池使用異步操作(NIO、事件)灶挟。

回到消息隊列的議題上琉朽,我們當然不希望消息的發(fā)送阻塞主流程(前面提到了,server端如果使用異步模型稚铣,則可能因消息合并帶來一定程度上的消息延遲)箱叁,所以可以先使用線程池提交一個發(fā)送請求,主流程繼續(xù)往下走惕医。

但是線程池中的請求關(guān)心結(jié)果嗎耕漱?Of course,必須等待服務(wù)端消息成功落地抬伺,才算是消息發(fā)送成功螟够。所以這里的模型,準確地說事客戶端半同步半異步(使用線程池不阻塞主流程峡钓,但線程池中的任務(wù)需要等待server端的返回)妓笙,server端是純異步∧苎遥客戶端的線程池wait在server端吐回的future上寞宫,直到server端處理完畢,才解除阻塞繼續(xù)進行捧灰。

總結(jié)一句淆九,同步能夠保證結(jié)果统锤,異步能夠保證效率毛俏,要合理的結(jié)合才能做到最好的效率炭庙。

批量

談到批量就不得不提生產(chǎn)者消費者模型。但生產(chǎn)者消費者模型中最大的痛點是:消費者到底應(yīng)該何時進行消費煌寇。大處著眼來看焕蹄,消費動作都是事件驅(qū)動的。主要事件包括:

攢夠了一定數(shù)量阀溶。

到達了一定時間腻脏。

隊列里有新的數(shù)據(jù)到來。

對于及時性要求高的數(shù)據(jù)银锻,可用采用方式3來完成永品,比如客戶端向服務(wù)端投遞數(shù)據(jù)击纬。只要隊列有數(shù)據(jù)鼎姐,就把隊列中的所有數(shù)據(jù)刷出,否則將自己掛起更振,等待新數(shù)據(jù)的到來炕桨。

在第一次把隊列數(shù)據(jù)往外刷的過程中,又積攢了一部分數(shù)據(jù)肯腕,第二次又可以形成一個批量献宫。偽代碼如下:

Executor executor = Executors.newFixedThreadPool(4);

final BlockingQueue queue = new ArrayBlockingQueue<>();

private Runnable task = new Runnable({//這里由于共享隊列,Runnable可以復(fù)用实撒,故做成全局的

public void run(){

List messages ?= new ArrayList<>(20);

queue.drainTo(messages姊途,20);

doSend(messages);//阻塞,在這個過程中會有新的消息到來知态,如果4個線程都占滿捷兰,隊列就有機會囤新的消息

}

});

public void send(Message message){

queue.offer(message);

executor.submit(task)

}

這種方式是消息延遲和批量的一個比較好的平衡,但優(yōu)先響應(yīng)低延遲肴甸。延遲的最高程度由上一次發(fā)送的等待時間決定寂殉。但可能造成的問題是發(fā)送過快的話批量的大小不夠滿足性能的極致。

Executor executor = Executors.newFixedThreadPool(4);

final BlockingQueue queue = new ArrayBlockingQueue<>();

volatile long last = System.currentMills();

Executors.newSingleThreadScheduledExecutor().submit(new Runnable(){

flush();

}原在,500友扰,500,TimeUnits.MILLS);

private Runnable task = new Runnable({//這里由于共享隊列庶柿,Runnable可以復(fù)用村怪,顧做成全局的。

public void run(){

List messages ?= new ArrayList<>(20);

queue.drainTo(messages浮庐,20);

doSend(messages);//阻塞甚负,在這個過程中會有新的消息到來柬焕,如果4個線程都占滿,隊列就有機會屯新的消息梭域。

}

});

public void send(Message message){

last = System.currentMills();

queue.offer(message);

flush();

}

private void flush(){

if(queue.size>200||System.currentMills()-last>200){

executor.submit(task)

}

}

相反對于可以用適量的延遲來換取高性能的場景來說斑举,用定時/定量二選一的方式可能會更為理想,既到達一定數(shù)量才發(fā)送病涨,但如果數(shù)量一直達不到富玷,也不能干等,有一個時間上限既穆。

具體說來赎懦,在上文的submit之前,多判斷一個時間和數(shù)量幻工,并且Runnable內(nèi)部維護一個定時器励两,避免沒有新任務(wù)到來時舊的任務(wù)永遠沒有機會觸發(fā)發(fā)送條件。對于server端的數(shù)據(jù)落地囊颅,使用這種方式就非常方便当悔。

最后啰嗦幾句,曾經(jīng)有人問我迁酸,為什么網(wǎng)絡(luò)請求小包合并成大包會提高性能先鱼?主要原因有兩個:

減少無謂的請求頭,如果你每個請求只有幾字節(jié)奸鬓,而頭卻有幾十字節(jié)焙畔,無疑效率非常低下。

減少回復(fù)的ack包個數(shù)串远。把請求合并后宏多,ack包數(shù)量必然減少,確認和重發(fā)的成本就會降低澡罚。

push 還是 pull 伸但?

上文提到的消息隊列,大多是針對push模型的設(shè)計×羯Γ現(xiàn)在市面上有很多經(jīng)典的也比較成熟的pull模型的消息隊列更胖,如Kafka、MetaQ等隔显。這跟JMS中傳統(tǒng)的push方式有很大的區(qū)別却妨,可謂另辟蹊徑。

我們簡要分析下push和pull模型各自存在的利弊括眠。

慢消費

慢消費無疑是push模型最大的致命傷彪标,穿成流水線來看,如果消費者的速度比發(fā)送者的速度慢很多掷豺,勢必造成消息在broker的堆積捞烟。假設(shè)這些消息都是有用的無法丟棄的薄声,消息就要一直在broker端保存。當然這還不是最致命的题画,最致命的是broker給consumer推送一堆consumer無法處理的消息默辨,consumer不是reject就是error,然后來回踢皮球婴程。

反觀pull模式廓奕,consumer可以按需消費抱婉,不用擔心自己處理不了的消息來騷擾自己档叔,而broker堆積消息也會相對簡單,無需記錄每一個要發(fā)送消息的狀態(tài)蒸绩,只需要維護所有消息的隊列和偏移量就可以了衙四。所以對于建立索引等慢消費,消息量有限且到來的速度不均勻的情況患亿,pull模式比較合適传蹈。

消息延遲與忙等

這是pull模式最大的短板。由于主動權(quán)在消費方步藕,消費方無法準確地決定何時去拉取最新的消息惦界。如果一次pull取到消息了還可以繼續(xù)去pull,如果沒有pull取到則需要等待一段時間重新pull咙冗。

但等待多久就很難判定了沾歪。你可能會說,我可以有xx動態(tài)pull取時間調(diào)整算法雾消,但問題的本質(zhì)在于灾搏,有沒有消息到來這件事情決定權(quán)不在消費方。也許1分鐘內(nèi)連續(xù)來了1000條消息立润,然后半個小時沒有新消息產(chǎn)生狂窑,可能你的算法算出下次最有可能到來的時間點是31分鐘之后,或者60分鐘之后桑腮,結(jié)果下條消息10分鐘后到了泉哈,是不是很讓人沮喪?

當然也不是說延遲就沒有解決方案了破讨,業(yè)界較成熟的做法是從短時間開始(不會對broker有太大負擔)丛晦,然后指數(shù)級增長等待。比如開始等5ms添忘,然后10ms采呐,然后20ms,然后40ms……直到有消息到來搁骑,然后再回到5ms斧吐。

即使這樣又固,依然存在延遲問題:假設(shè)40ms到80ms之間的50ms消息到來,消息就延遲了30ms煤率,而且對于半個小時來一次的消息仰冠,這些開銷就是白白浪費的。

在阿里的RocketMq里蝶糯,有一種優(yōu)化的做法-長輪詢洋只,來平衡推拉模型各自的缺點≈绾矗基本思路是:消費者如果嘗試拉取失敗识虚,不是直接return,而是把連接掛在那里wait,服務(wù)端如果有新的消息到來,把連接notify起來妒茬,這也是不錯的思路担锤。但海量的長連接block對系統(tǒng)的開銷還是不容小覷的,還是要合理的評估時間間隔乍钻,給wait加一個時間上限比較好~

順序消息

如果push模式的消息隊列肛循,支持分區(qū),單分區(qū)只支持一個消費者消費银择,并且消費者只有確認一個消息消費后才能push送另外一個消息多糠,還要發(fā)送者保證全局順序唯一,聽起來也能做順序消息浩考,但成本太高了夹孔,尤其是必須每個消息消費確認后才能發(fā)下一條消息,這對于本身堆積能力和慢消費就是瓶頸的push模式的消息隊列怀挠,簡直是一場災(zāi)難析蝴。

反觀pull模式,如果想做到全局順序消息绿淋,就相對容易很多:

producer對應(yīng)partition闷畸,并且單線程。

consumer對應(yīng)partition吞滞,消費確認(或批量確認)佑菩,繼續(xù)消費即可。

所以對于日志push送這種最好全局有序裁赠,但允許出現(xiàn)小誤差的場景殿漠,pull模式非常合適。如果你不想看到通篇亂套的日志~~

Anyway佩捞,需要順序消息的場景還是比較有限的而且成本太高绞幌,請慎重考慮。

總結(jié)

本文從為何使用消息隊列開始講起一忱,然后主要介紹了如何從零開始設(shè)計一個消息隊列莲蜘,包括RPC谭确、事務(wù)、最終一致性票渠、廣播逐哈、消息確認等關(guān)鍵問題。并對消息隊列的push问顷、pull模型做了簡要分析昂秃,最后從批量和異步角度,分析了消息隊列性能優(yōu)化的思路杜窄。本文主要是源自自己在開發(fā)消息隊列中的思考和讀源碼時的體會肠骆,比較不"官方",也難免會存在一些漏洞羞芍,歡迎大家多多交流哗戈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荷科,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纱注,老刑警劉巖畏浆,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異狞贱,居然都是意外死亡刻获,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進店門瞎嬉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝎毡,“玉大人,你說我怎么就攤上這事氧枣°灞” “怎么了?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵便监,是天一觀的道長扎谎。 經(jīng)常有香客問我,道長烧董,這世上最難降的妖魔是什么毁靶? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮逊移,結(jié)果婚禮上预吆,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好爹脾,可當我...
    茶點故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著骡苞,像睡著了一般巷嚣。 火紅的嫁衣襯著肌膚如雪喘先。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天廷粒,我揣著相機與錄音窘拯,去河邊找鬼。 笑死坝茎,一個胖子當著我的面吹牛涤姊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嗤放,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼思喊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了次酌?” 一聲冷哼從身側(cè)響起恨课,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎岳服,沒想到半個月后剂公,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡吊宋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年纲辽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片璃搜。...
    茶點故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡拖吼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出这吻,到底是詐尸還是另有隱情吊档,我是刑警寧澤,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布橘原,位于F島的核電站籍铁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏趾断。R本人自食惡果不足惜拒名,卻給世界環(huán)境...
    茶點故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芋酌。 院中可真熱鬧增显,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至炸站,卻和暖如春星澳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旱易。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工禁偎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人阀坏。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓如暖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親忌堂。 傳聞我的和親對象是個殘疾皇子盒至,可洞房花燭夜當晚...
    茶點故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 消息隊列設(shè)計精要 消息隊列已經(jīng)逐漸成為企業(yè)IT系統(tǒng)內(nèi)部通信的核心手段。它具有低耦合士修、可靠投遞枷遂、廣播、流量控制李命、最終...
    meng_philip123閱讀 1,505評論 1 25
  • 消息隊列已經(jīng)逐漸成為企業(yè)IT系統(tǒng)內(nèi)部通信的核心手段登淘。它具有低耦合、可靠投遞封字、廣播、流量控制耍鬓、最終一致性等一系列功能...
    Sophie12138閱讀 721評論 0 7
  • ——親情 里三層 外三層 密不過 椿萱之恩 ——友情 里三行 外三行 不如空一行 ——愛情 她是匣板 他就是抽屜 ...
    葉新涼閱讀 386評論 0 1
  • 今天用Xcode打包之后發(fā)現(xiàn)找不到dsym文件阔籽,無論是在Window->Organizer中點擊Download ...
    張囧瑞閱讀 2,328評論 0 2
  • 這棵樹很美
    上善若水811閱讀 241評論 0 0