RabbitMQ之镜像队列
歡迎支持筆者新作:《深入理解Kafka:核心設(shè)計(jì)與實(shí)踐原理》和《RabbitMQ實(shí)戰(zhàn)指南》,同時(shí)歡迎關(guān)注筆者的微信公眾號(hào):朱小廝的博客。
歡迎跳轉(zhuǎn)到本文的原文鏈接:https://honeypps.com/mq/rabbitmq-mirror-queue/
概述
如果RabbitMQ集群只有一個(gè)broker節(jié)點(diǎn),那么該節(jié)點(diǎn)的失效將導(dǎo)致整個(gè)服務(wù)臨時(shí)性的不可用,并且可能會(huì)導(dǎo)致message的丟失(尤其是在非持久化message存儲(chǔ)于非持久化queue中的時(shí)候)。當(dāng)然可以將所有的publish的message都設(shè)置為持久化的,并且使用持久化的queue,但是這樣仍然無法避免由于緩存導(dǎo)致的問題:因?yàn)閙essage在發(fā)送之后和被寫入磁盤并執(zhí)行fsync之間存在一個(gè)雖然短暫但是會(huì)產(chǎn)生問題的時(shí)間窗。通過publisher的confirm機(jī)制能夠確保客戶端知道哪些message已經(jīng)存入磁盤,盡管如此,一般不希望遇到因單點(diǎn)故障導(dǎo)致的服務(wù)不可用。
如果RabbitMQ集群是由多個(gè)broker節(jié)點(diǎn)構(gòu)成的,那么從服務(wù)的整體可用性上來講,該集群對(duì)于單點(diǎn)失效是有彈性的,但是同時(shí)也需要注意:盡管exchange和binding能夠在單點(diǎn)失效問題上幸免于難,但是queue和其上持有的message卻不行,這是因?yàn)閝ueue及其內(nèi)容僅僅存儲(chǔ)于單個(gè)節(jié)點(diǎn)之上,所以一個(gè)節(jié)點(diǎn)的失效表現(xiàn)為其對(duì)應(yīng)的queue不可用。
引入RabbitMQ的鏡像隊(duì)列機(jī)制,將queue鏡像到cluster中其他的節(jié)點(diǎn)之上。在該實(shí)現(xiàn)下,如果集群中的一個(gè)節(jié)點(diǎn)失效了,queue能自動(dòng)地切換到鏡像中的另一個(gè)節(jié)點(diǎn)以保證服務(wù)的可用性。在通常的用法中,針對(duì)每一個(gè)鏡像隊(duì)列都包含一個(gè)master和多個(gè)slave,分別對(duì)應(yīng)于不同的節(jié)點(diǎn)。slave會(huì)準(zhǔn)確地按照master執(zhí)行命令的順序進(jìn)行命令執(zhí)行,故slave與master上維護(hù)的狀態(tài)應(yīng)該是相同的。除了publish外所有動(dòng)作都只會(huì)向master發(fā)送,然后由master將命令執(zhí)行的結(jié)果廣播給slave們,故看似從鏡像隊(duì)列中的消費(fèi)操作實(shí)際上是在master上執(zhí)行的。
一旦完成了選中的slave被提升為master的動(dòng)作,發(fā)送到鏡像隊(duì)列的message將不會(huì)再丟失:publish到鏡像隊(duì)列的所有消息總是被直接publish到master和所有的slave之上。這樣一旦master失效了,message仍然可以繼續(xù)發(fā)送到其他slave上。
RabbitMQ的鏡像隊(duì)列同時(shí)支持publisher confirm和事務(wù)兩種機(jī)制。在事務(wù)機(jī)制中,只有當(dāng)前事務(wù)在全部鏡像queue中執(zhí)行之后,客戶端才會(huì)收到Tx.CommitOk的消息。同樣的,在publisher confirm機(jī)制中,向publisher進(jìn)行當(dāng)前message確認(rèn)的前提是該message被全部鏡像所接受了。
鏡像隊(duì)列的設(shè)置
鏡像隊(duì)列的配置通過添加policy完成,policy添加的命令為:
rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]-p Vhost: 可選參數(shù),針對(duì)指定vhost下的queue進(jìn)行設(shè)置 Name: policy的名稱 Pattern: queue的匹配模式(正則表達(dá)式) Definition:鏡像定義,包括三個(gè)部分ha-mode, ha-params, ha-sync-modeha-mode:指明鏡像隊(duì)列的模式,有效值為 all/exactly/nodesall:表示在集群中所有的節(jié)點(diǎn)上進(jìn)行鏡像exactly:表示在指定個(gè)數(shù)的節(jié)點(diǎn)上進(jìn)行鏡像,節(jié)點(diǎn)的個(gè)數(shù)由ha-params指定nodes:表示在指定的節(jié)點(diǎn)上進(jìn)行鏡像,節(jié)點(diǎn)名稱通過ha-params指定ha-params:ha-mode模式需要用到的參數(shù)ha-sync-mode:進(jìn)行隊(duì)列中消息的同步方式,有效值為automatic和manual priority:可選參數(shù),policy的優(yōu)先級(jí)例如,對(duì)隊(duì)列名稱以“queue_”開頭的所有隊(duì)列進(jìn)行鏡像,并在集群的兩個(gè)節(jié)點(diǎn)上完成進(jìn)行,policy的設(shè)置命令為:
rabbitmqctl set_policy --priority 0 --apply-to queues mirror_queue "^queue_" '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'也可以通過RabbitMQ的web管理界面設(shè)置:
或者通過HTTP API的方式,詳細(xì)可以參考官方文檔:Highly Available (Mirrored) Queues
鏡像隊(duì)列的原理
普通MQ的結(jié)構(gòu)
通常隊(duì)列由兩部分組成:一部分是AMQQueue,負(fù)責(zé)AMQP協(xié)議相關(guān)的消息處理,即接收生產(chǎn)者發(fā)布的消息、向消費(fèi)者投遞消息、處理消息confirm、acknowledge等等;另一部分是BackingQueue,它提供了相關(guān)的接口供AMQQueue調(diào)用,完成消息的存儲(chǔ)以及可能的持久化工作等。
在RabbitMQ中BackingQueue又由5個(gè)子隊(duì)列組成:Q1, Q2, Delta, Q3和Q4。RabbitMQ中的消息一旦進(jìn)入隊(duì)列,不是固定不變的,它會(huì)隨著系統(tǒng)的負(fù)載在隊(duì)列中不斷流動(dòng),消息的不斷發(fā)生變化。與這5個(gè)子隊(duì)列對(duì)于,在BackingQueue中消息的生命周期分為4個(gè)狀態(tài):
- Alpha:消息的內(nèi)容和消息索引都在RAM中。Q1和Q4的狀態(tài)。
- Beta:消息的內(nèi)容保存在DISK上,消息索引保存在RAM中。Q2和Q3的狀態(tài)。
- Gamma:消息內(nèi)容保存在DISK上,消息索引在DISK和RAM都有。Q2和Q3的狀態(tài)。
- Delta:消息內(nèi)容和索引都在DISK上。Delta的狀態(tài)。
注意:對(duì)于持久化的消息,消息內(nèi)容和消息所有都必須先保存在DISK上,才會(huì)處于上述狀態(tài)中的一種,而Gamma狀態(tài)的消息是只有持久化的消息才會(huì)有的狀態(tài)。
上述就是RabbitMQ的多層隊(duì)列結(jié)構(gòu)的設(shè)計(jì),我們可以看出從Q1到Q4,基本經(jīng)歷RAM->DISK->RAM這樣的過程。這樣設(shè)計(jì)的好處是:當(dāng)隊(duì)列負(fù)載很高的情況下,能夠通過將一部分消息由磁盤保存來節(jié)省內(nèi)存空間,當(dāng)負(fù)載降低的時(shí)候,這部分消息又漸漸回到內(nèi)存,被消費(fèi)者獲取,使得整個(gè)隊(duì)列具有很好的彈性。下面我們就來看一下,整個(gè)消息隊(duì)列的工作流程。
引起消息流動(dòng)主要有兩方面因素:其一是消費(fèi)者獲取消息;其二是由于內(nèi)存不足引起消息換出到磁盤。RabbitMQ在系統(tǒng)運(yùn)行時(shí)會(huì)根據(jù)消息傳輸?shù)乃俣扔?jì)算一個(gè)當(dāng)前內(nèi)存中能夠保存的最大消息數(shù)量(Target_RAM_Count),當(dāng)內(nèi)存中的消息數(shù)量大于該值時(shí),就會(huì)引起消息的流動(dòng)。進(jìn)入隊(duì)列的消息,一般會(huì)按照Q1->Q2->Delta->Q3->Q4的順序進(jìn)行流動(dòng),但是并不是每條消息都一定會(huì)經(jīng)歷所有的狀態(tài),這個(gè)取決于當(dāng)前系統(tǒng)的負(fù)載狀況。
當(dāng)消費(fèi)者獲取消息時(shí),首先會(huì)從Q4隊(duì)列中獲取消息,如果Q4獲取成功,則返回。如果Q4為空,則嘗試從Q3獲取消息,首先系統(tǒng)會(huì)判斷Q3是否為空,如果為空則返回隊(duì)列為空,即此時(shí)隊(duì)列中無消息(后續(xù)會(huì)論證)。如果不為空,則取出Q3的消息,然后判斷此時(shí)Q3和Delta隊(duì)列的長(zhǎng)度,如果都為空,則可認(rèn)為Q2、Delta、Q3、Q4全部為空(后續(xù)會(huì)論證),此時(shí)將Q1中消息直接轉(zhuǎn)移到Q4中,下次直接從Q4中獲取消息。如果Q3為空,Delta不為空,則將Delta轉(zhuǎn)移到Q3中,如果Q3不為空,則直接下次從Q3中獲取消息。在將Delta轉(zhuǎn)移到Q3的過程中,RabbitMQ是按照索引分段讀取的,首先讀取某一段,直到讀到的消息非空為止,然后判斷讀取的消息個(gè)數(shù)與Delta中的消息個(gè)數(shù)是否相等,如果相等,則斷定此時(shí)Delta中已無消息,則直接將Q2和剛讀到的消息一并放入Q3中。如果不相等,則僅將此次讀取到的消息轉(zhuǎn)移到Q3。這就是消費(fèi)者引起的消息流動(dòng)過程。
消息換出的條件是內(nèi)存中保存的消息數(shù)量+等待ACK的消息的數(shù)量>Target_RAM_Count。當(dāng)條件出發(fā)時(shí),系統(tǒng)首先會(huì)判斷如果當(dāng)前進(jìn)入等待ACK的消息的速度大于進(jìn)入隊(duì)列的消息的速度時(shí),會(huì)先處理等待ACK的消息。
最后我們來分析一下前面遺留的兩個(gè)問題,一個(gè)是為什么Q3隊(duì)列為空即可以認(rèn)定整個(gè)隊(duì)列為空。試想如果Q3為空,Delta不空,則在Q3取出最后一條消息時(shí),Delta上的消息就會(huì)被轉(zhuǎn)移到Q3上,Q3空矛盾。如果Q2不空,則在Q3取出最后一條消息,如果Delta為空,則會(huì)將Q2的消息并入到Q3,與Q3為空矛盾。如果Q1不為空,則在Q3取出最后一條消息,如果Delta和Q3均為空時(shí),則將Q1的消息轉(zhuǎn)移到Q4中,與Q4為空矛盾。這也解釋了另外一個(gè)問題,即為什么Q3和Delta為空,Q2就為空。
通常在負(fù)載正常時(shí),如果消息被消費(fèi)的速度不小于接收新消息的速度,對(duì)于不需要保證可靠不丟的消息極可能只會(huì)有Alpha狀態(tài)。對(duì)于durable=true的消息,它一定會(huì)進(jìn)入gamma狀態(tài),若開啟publish confirm機(jī)制,只有到了這個(gè)階段才會(huì)確認(rèn)該消息已經(jīng)被接受,若消息消費(fèi)速度足夠快,內(nèi)存也充足,這些消息也不會(huì)繼續(xù)走到下一狀態(tài)。
通常在系統(tǒng)負(fù)載較高時(shí),已接受到的消息若不能很快被消費(fèi)掉,這些消息就會(huì)進(jìn)入到很深的隊(duì)列中去,增加處理每個(gè)消息的平均開銷。因?yàn)橐ǜ嗟臅r(shí)間和資源處理“積壓”的消息,所以用于處理新來的消息的能力就會(huì)降低,使得后來的消息又被積壓進(jìn)入很深的隊(duì)列,繼續(xù)加大處理每個(gè)消息的平均開銷,這樣情況就會(huì)越來越惡化,使得系統(tǒng)的處理能力大大降低。
根據(jù)官網(wǎng)資料,應(yīng)對(duì)這一問題,有三個(gè)措施:
鏡像隊(duì)列的結(jié)構(gòu)
鏡像隊(duì)列基本上就是一個(gè)特殊的BackingQueue,它內(nèi)部包裹了一個(gè)普通的BackingQueue做本地消息持久化處理,在此基礎(chǔ)上增加了將消息和ack復(fù)制到所有鏡像的功能。所有對(duì)mirror_queue_master的操作,會(huì)通過組播GM(下面會(huì)講到)的方式同步到各slave節(jié)點(diǎn)。GM負(fù)責(zé)消息的廣播,mirror_queue_slave負(fù)責(zé)回調(diào)處理,而master上的回調(diào)處理是由coordinator負(fù)責(zé)完成。mirror_queue_slave中包含了普通的BackingQueue進(jìn)行消息的存儲(chǔ),master節(jié)點(diǎn)中BackingQueue包含在mirror_queue_master中由AMQQueue進(jìn)行調(diào)用。
消息的發(fā)布(除了Basic.Publish之外)與消費(fèi)都是通過master節(jié)點(diǎn)完成。master節(jié)點(diǎn)對(duì)消息進(jìn)行處理的同時(shí)將消息的處理動(dòng)作通過GM廣播給所有的slave節(jié)點(diǎn),slave節(jié)點(diǎn)的GM收到消息后,通過回調(diào)交由mirror_queue_slave進(jìn)行實(shí)際的處理。
對(duì)于Basic.Publish,消息同時(shí)發(fā)送到master和所有slave上,如果此時(shí)master宕掉了,消息還發(fā)送slave上,這樣當(dāng)slave提升為master的時(shí)候消息也不會(huì)丟失。
GM, Guarenteed Multicast. GM模塊實(shí)現(xiàn)的一種可靠的組播通訊協(xié)議,該協(xié)議能夠保證組播消息的原子性,即保證組中活著的節(jié)點(diǎn)要么都收到消息要么都收不到。它的實(shí)現(xiàn)大致如下:
將所有的節(jié)點(diǎn)形成一個(gè)循環(huán)鏈表,每個(gè)節(jié)點(diǎn)都會(huì)監(jiān)控位于自己左右兩邊的節(jié)點(diǎn),當(dāng)有節(jié)點(diǎn)新增時(shí),相鄰的節(jié)點(diǎn)保證當(dāng)前廣播的消息會(huì)復(fù)制到新的節(jié)點(diǎn)上;當(dāng)有節(jié)點(diǎn)失效時(shí),相鄰的節(jié)點(diǎn)會(huì)接管保證本次廣播的消息會(huì)復(fù)制到所有的節(jié)點(diǎn)。在master節(jié)點(diǎn)和slave節(jié)點(diǎn)上的這些gm形成一個(gè)group,group(gm_group)的信息會(huì)記錄在mnesia中。不同的鏡像隊(duì)列形成不同的group。消息從master節(jié)點(diǎn)對(duì)于的gm發(fā)出后,順著鏈表依次傳送到所有的節(jié)點(diǎn),由于所有節(jié)點(diǎn)組成一個(gè)循環(huán)鏈表,master節(jié)點(diǎn)對(duì)應(yīng)的gm最終會(huì)收到自己發(fā)送的消息,這個(gè)時(shí)候master節(jié)點(diǎn)就知道消息已經(jīng)復(fù)制到所有的slave節(jié)點(diǎn)了。
新增節(jié)點(diǎn)
新節(jié)點(diǎn)的加入過程如下圖所示:
每當(dāng)一個(gè)節(jié)點(diǎn)加入或者重新加入(例如從網(wǎng)絡(luò)分區(qū)中恢復(fù)過來)鏡像隊(duì)列,之前保存的隊(duì)列內(nèi)容會(huì)被清空。
節(jié)點(diǎn)的失效
如果某個(gè)slave失效了,系統(tǒng)處理做些記錄外幾乎啥都不做:master依舊是master,客戶端不需要采取任何行動(dòng),或者被通知slave失效。
如果master失效了,那么slave中的一個(gè)必須被選中為master。被選中作為新的master的slave通常是最老的那個(gè),因?yàn)樽罾系膕lave與前任master之間的同步狀態(tài)應(yīng)該是最好的。然而,需要注意的是,如果存在沒有任何一個(gè)slave與master完全同步的情況,那么前任master中未被同步的消息將會(huì)丟失。
消息的同步
將新節(jié)點(diǎn)加入已存在的鏡像隊(duì)列是,默認(rèn)情況下ha-sync-mode=manual,鏡像隊(duì)列中的消息不會(huì)主動(dòng)同步到新節(jié)點(diǎn),除非顯式調(diào)用同步命令。當(dāng)調(diào)用同步命令后,隊(duì)列開始阻塞,無法對(duì)其進(jìn)行操作,直到同步完畢。當(dāng)ha-sync-mode=automatic時(shí),新加入節(jié)點(diǎn)時(shí)會(huì)默認(rèn)同步已知的鏡像隊(duì)列。由于同步過程的限制,所以不建議在生產(chǎn)的active隊(duì)列(有生產(chǎn)消費(fèi)消息)中操作。
可以使用下面的命令來查看那些slaves已經(jīng)完成同步:
rabbitmqctl list_queues name slave_pids synchronised_slave_pids可以通過手動(dòng)的方式同步一個(gè)queue:
rabbitmqctl sync_queue name同樣也可以取消某個(gè)queue的同步功能:
rabbitmqctl cancel_sync_queue name當(dāng)然這些都可以通過management插件來設(shè)置。
補(bǔ)充要點(diǎn)
鏡像隊(duì)列不能作為負(fù)載均衡使用,因?yàn)槊總€(gè)操作在所有節(jié)點(diǎn)都要做一遍。
ha-mode參數(shù)和durable declare對(duì)exclusive隊(duì)列都并不生效,因?yàn)閑xclusive隊(duì)列是連接獨(dú)占的,當(dāng)連接斷開,隊(duì)列自動(dòng)刪除。所以實(shí)際上這兩個(gè)參數(shù)對(duì)exclusive隊(duì)列沒有意義。
當(dāng)所有slave都出在(與master)未同步狀態(tài)時(shí),并且ha-promote-on-shutdown設(shè)置為when-synced(默認(rèn))時(shí),如果master因?yàn)橹鲃?dòng)的原因停掉,比如是通過rabbitmqctl stop命令停止或者優(yōu)雅關(guān)閉OS,那么slave不會(huì)接管master,也就是此時(shí)鏡像隊(duì)列不可用;但是如果master因?yàn)楸粍?dòng)原因停掉,比如VM或者OS crash了,那么slave會(huì)接管master。這個(gè)配置項(xiàng)隱含的價(jià)值取向是保證消息可靠不丟失,放棄可用性。如果ha-promote-on-shutdown設(shè)置為always,那么不論master因?yàn)楹畏N原因停止,slave都會(huì)接管master,優(yōu)先保證可用性。
鏡像隊(duì)列中最后一個(gè)停止的節(jié)點(diǎn)會(huì)是master,啟動(dòng)順序必須是master先啟動(dòng),如果slave先啟動(dòng),它會(huì)有30s的等待時(shí)間,等待master的啟動(dòng),然后加入cluster中(如果30s內(nèi)master沒有啟動(dòng),slave會(huì)自動(dòng)停止)。當(dāng)所有節(jié)點(diǎn)因故(斷電等)同時(shí)離線時(shí),每個(gè)節(jié)點(diǎn)都認(rèn)為自己不是最后一個(gè)停止的節(jié)點(diǎn)。要恢復(fù)鏡像隊(duì)列,可以嘗試在30s之內(nèi)啟動(dòng)所有節(jié)點(diǎn)。
對(duì)于鏡像隊(duì)列,客戶端Basic.Publish操作會(huì)同步到所有節(jié)點(diǎn)(消息同時(shí)發(fā)送到master和所有slave上,如果此時(shí)master宕掉了,消息還發(fā)送slave上,這樣當(dāng)slave提升為master的時(shí)候消息也不會(huì)丟失),而其他操作則是通過master中轉(zhuǎn),再由master將操作作用于slave。比如一個(gè)Basic.Get操作,假如客戶端與slave建立了TCP連接,首先是slave將Basic.Get請(qǐng)求發(fā)送至master,由master備好數(shù)據(jù),返回至slave,投遞給消費(fèi)者。
當(dāng)slave宕掉了,除了與slave相連的客戶端連接全部斷開之外,沒有其他影響。
當(dāng)master宕掉時(shí),會(huì)有以下連鎖反應(yīng):
鏡像隊(duì)列的恢復(fù)
前提:兩個(gè)節(jié)點(diǎn)A和B組成以鏡像隊(duì)列。
場(chǎng)景1:A先停,B后停
該場(chǎng)景下B是master,只要先啟動(dòng)B,再啟動(dòng)A即可?;蛘呦葐?dòng)A,再在30s之內(nèi)啟動(dòng)B即可恢復(fù)鏡像隊(duì)列。(如果沒有在30s內(nèi)回復(fù)B,那么A自己就停掉自己)
場(chǎng)景2:A,B同時(shí)停
該場(chǎng)景下可能是由掉電等原因造成,只需在30s內(nèi)聯(lián)系啟動(dòng)A和B即可恢復(fù)鏡像隊(duì)列。
場(chǎng)景3:A先停,B后停,且A無法恢復(fù)。
因?yàn)锽是master,所以等B起來后,在B節(jié)點(diǎn)上調(diào)用rabbitmqctl forget_cluster_node A以接觸A的cluster關(guān)系,再將新的slave節(jié)點(diǎn)加入B即可重新恢復(fù)鏡像隊(duì)列。
場(chǎng)景4:A先停,B后停,且B無法恢復(fù)
該場(chǎng)景比較難處理,舊版本的RabbitMQ沒有有效的解決辦法,在現(xiàn)在的版本中,因?yàn)锽是master,所以直接啟動(dòng)A是不行的,當(dāng)A無法啟動(dòng)時(shí),也就沒版本在A節(jié)點(diǎn)上調(diào)用rabbitmqctl forget_cluster_node B了,新版本中forget_cluster_node支持-offline參數(shù),offline參數(shù)允許rabbitmqctl在離線節(jié)點(diǎn)上執(zhí)行forget_cluster_node命令,迫使RabbitMQ在未啟動(dòng)的slave節(jié)點(diǎn)中選擇一個(gè)作為master。當(dāng)在A節(jié)點(diǎn)執(zhí)行rabbitmqctl forget_cluster_node -offline B時(shí),RabbitMQ會(huì)mock一個(gè)節(jié)點(diǎn)代表A,執(zhí)行forget_cluster_node命令將B提出cluster,然后A就能正常啟動(dòng)了。最后將新的slave節(jié)點(diǎn)加入A即可重新恢復(fù)鏡像隊(duì)列
場(chǎng)景5:A先停,B后停,且A和B均無法恢復(fù),但是能得到A或B的磁盤文件
這個(gè)場(chǎng)景更加難以處理。將A或B的數(shù)據(jù)庫文件($RabbitMQ_HOME/var/lib目錄中)copy至新節(jié)點(diǎn)C的目錄下,再將C的hostname改成A或者B的hostname。如果copy過來的是A節(jié)點(diǎn)磁盤文件,按場(chǎng)景4處理,如果拷貝過來的是B節(jié)點(diǎn)的磁盤文件,按場(chǎng)景3處理。最后將新的slave節(jié)點(diǎn)加入C即可重新恢復(fù)鏡像隊(duì)列。
場(chǎng)景6:A先停,B后停,且A和B均無法恢復(fù),且無法得到A和B的磁盤文件
無解。
參考資料
歡迎跳轉(zhuǎn)到本文的原文鏈接:https://honeypps.com/mq/rabbitmq-mirror-queue/
歡迎支持筆者新作:《深入理解Kafka:核心設(shè)計(jì)與實(shí)踐原理》和《RabbitMQ實(shí)戰(zhàn)指南》,同時(shí)歡迎關(guān)注筆者的微信公眾號(hào):朱小廝的博客。
總結(jié)
以上是生活随笔為你收集整理的RabbitMQ之镜像队列的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Conclusion]RabbitMQ
- 下一篇: 为什么QueueingConsumer会