前言
這將是RocketMQ實(shí)戰(zhàn)系列的最后一篇文章,該系列的文章列表如下:
《RocketMQ實(shí)戰(zhàn)(三):分布式事務(wù)》
RocketMQ 3.2.6的事務(wù)機(jī)制
在上一篇博客中,已經(jīng)知道RocketMQ 3.0.8是支持事務(wù)回查機(jī)制,但是在RocketMQ 3.2.6中取消了這個(gè)功能,下面我們繼續(xù)以轉(zhuǎn)賬功能分析我們自己如何解決這個(gè)問(wèn)題。
在正常情況下,當(dāng)然沒(méi)有問(wèn)題,如果第五步(向MQ發(fā)送確認(rèn)消息)出現(xiàn)失敗,加上RocketMQ 3.2.6版本沒(méi)有事務(wù)回查機(jī)制,就會(huì)導(dǎo)致這條轉(zhuǎn)賬消息,在A銀行完成了操作,但是遲遲對(duì)B銀行系統(tǒng)不可見!
用戶U1從A銀行系統(tǒng)轉(zhuǎn)賬給B銀行系統(tǒng)的用戶U2的處理過(guò)程如下:
第一步:A銀行系統(tǒng)生成一條轉(zhuǎn)賬消息,以事務(wù)消息的方式寫入RocketMQ,此時(shí)B銀行系統(tǒng)不可見這條消息
第二步:寫入MQ成功后,回調(diào)A銀行系統(tǒng),對(duì)T1,T2表進(jìn)行操作(很顯然需要是一個(gè)事務(wù))
我們重點(diǎn)關(guān)注下T2表,這個(gè)表是用來(lái)干嘛的呢?每條轉(zhuǎn)賬消息都會(huì)在T2表中,該表有2個(gè)特殊的字段:status,updatetime。(用途會(huì)在后文詳述)
第三步:完成第二步,接下來(lái)發(fā)送確認(rèn)消息給MQ,如果這個(gè)確認(rèn)消息發(fā)送成功,那么這條轉(zhuǎn)賬消息,將對(duì)B銀行系統(tǒng)可見。然后B銀行系統(tǒng),會(huì)在一個(gè)事務(wù)中完成對(duì)t3,t5的操作。
如果發(fā)送確認(rèn)消息給MQ失敗的處理思路:
首先,B銀行系統(tǒng),有一個(gè)定時(shí)任務(wù)(比如說(shuō)每隔1MIN執(zhí)行一次),掃描表t5,取得一段時(shí)間內(nèi)的數(shù)據(jù),發(fā)送給A銀行系統(tǒng)。要知道t5中的數(shù)據(jù),必然是A銀行系統(tǒng)成功處理并發(fā)送確認(rèn)消息成功的轉(zhuǎn)賬數(shù)據(jù)。為什么要發(fā)送給A銀行系統(tǒng)呢,其實(shí)就是為了找到那些發(fā)送確認(rèn)消息失敗的轉(zhuǎn)賬數(shù)據(jù)。那么怎么發(fā)給A銀行系統(tǒng)呢,這個(gè)方式比較多,可以考慮在來(lái)一個(gè)Topic,也可以考慮Netty等。發(fā)送給A銀行系統(tǒng),其實(shí)就是為了更新t2表的status,updatetime。
這里有一個(gè)關(guān)鍵,如何“掃描表t5,取得一段時(shí)間內(nèi)的數(shù)據(jù)”?這就是t4的作用,在t4中記錄一個(gè)time字段,每次定時(shí)任務(wù)啟動(dòng),先更新time(比如設(shè)定為當(dāng)前系統(tǒng)時(shí)間,設(shè)置前的的時(shí)間為old),然后掃描出t5中大于這個(gè)old時(shí)間的轉(zhuǎn)賬數(shù)據(jù),如此循環(huán)往復(fù)。
其次,A銀行系統(tǒng),也有一個(gè)定時(shí)任務(wù)(可以根據(jù)業(yè)務(wù)消費(fèi)能力定,可以大一些),掃描t2表(指定status及updatetime條件),將那些確認(rèn)消息發(fā)送失敗的轉(zhuǎn)賬消息找出來(lái),更新updatetime并發(fā)送給MQ。
這樣,我們并沒(méi)有改動(dòng)RocketMQ 3.2.6的源碼,而是在外圍解決了事務(wù)回查!
其實(shí)到這里,你可以發(fā)現(xiàn)RocketMQ的一個(gè)特點(diǎn),就是將生產(chǎn)者和MQ綁定,而不需要特別處理消費(fèi)者,這是為什么呢?因?yàn)橄⒅灰l(fā)往RocketMQ成功,那么就意味著成功,為什么這么說(shuō)?
前面,我們說(shuō)過(guò),消費(fèi)者端消費(fèi)消息只會(huì)產(chǎn)生2種錯(cuò)誤,第一:timeout,第二:exception。要知道RocketMQ對(duì)于超時(shí),會(huì)不斷重試;對(duì)于消費(fèi)異常,會(huì)根據(jù)消費(fèi)端的返回碼,會(huì)有重試機(jī)制保證。也就是,RocketMQ一定會(huì)讓消息得到消費(fèi),如果消費(fèi)有問(wèn)題,只能是消費(fèi)者的問(wèn)題,而不會(huì)是RocketMQ的問(wèn)題!
Pull Or Push
在前面的博客已經(jīng)提到,在RocketMQ中Consumer分為2類:Push Consumer、Pull Consumer。以前的例子都是Push Consumer,接下來(lái),為大家介紹下Pull Consumer。
從表面意思上來(lái)看,好像Push是MQ推送給消費(fèi)者,而Pull是消費(fèi)者從MQ中拉取;其實(shí)本質(zhì)上都是拉取模式PULL,即消費(fèi)者從MQ中輪詢?nèi)〉孟ⅰ?/b>
在Push模式下,Consumer把輪詢過(guò)程封裝了,并注冊(cè)了MessageListener監(jiān)聽器,取到消息后,喚醒MessageListener監(jiān)聽器中的consumeMessage()進(jìn)行消費(fèi),所以給我們?cè)斐闪烁杏X(jué)上好像是“推消息”。
在Pull模式下,需要特別注意的是,本質(zhì)上是從一個(gè)Topic下的所有Queue進(jìn)行拉取,而且每個(gè)Queue都必須記錄拉取位置,否則會(huì)導(dǎo)致重復(fù)消費(fèi)。還有拉取的時(shí)間間隔,拉取的大小等等。不過(guò)所有的這一切,MQPullConsumerScheduleService都替我們考慮清楚了,提供updateConsumeOffset去更新消費(fèi)的隊(duì)列的位置(默認(rèn)5S同步一次),提供setPullNextDelayTimeMillis設(shè)置下次拉取的時(shí)間間隔(應(yīng)該設(shè)置的大一些,至少大于5S)。
仔細(xì)回想下,對(duì)于Push方式的回調(diào) ? 和 ?Pull方式的回調(diào),還有什么關(guān)鍵區(qū)別么?
對(duì)于Push而言,不論是基于MessageListenerConcurrently的,還是基于MessageListenerOrderly的,都有返回值的;而Pull的doPullTask的返回值卻是void?
這意味,我們需要在pull方式中,注意自己處理每條消息消費(fèi)的異常情況!
通過(guò)運(yùn)行結(jié)果,可以印證上面的觀點(diǎn):為什么每次消費(fèi)都是4條開始,4條結(jié)束呢?因?yàn)橐粋€(gè)Topic下有4個(gè)Queue,而且上面的代碼實(shí)際上會(huì)針對(duì)每個(gè)Queue開啟一個(gè)線程去消費(fèi)!
RocketMQ Filter組件介紹
對(duì)于ActiveMQ而言,我們可以通過(guò)JMS Selectors機(jī)制(就是類似于SQL的語(yǔ)法)來(lái)實(shí)現(xiàn)過(guò)濾,很easy。那么和RocketMQ Filter組件有什么區(qū)別呢?
雖然,2者都能實(shí)現(xiàn)過(guò)濾,但是RocketMQ Filter的性能要更高效些,因?yàn)镽ocketMQ是在broker上將過(guò)濾后的數(shù)據(jù)發(fā)往filter,然后消費(fèi)者直接從filter上取得數(shù)據(jù);而ActiveMQ是消費(fèi)者直接在broker上進(jìn)行過(guò)濾消費(fèi)!(當(dāng)然,對(duì)于RocketMQ而言,Tag機(jī)制已經(jīng)足夠應(yīng)付日常絕大數(shù)的過(guò)濾功能,除非你的業(yè)務(wù)對(duì)性能有特別高的要求)
具體怎么做呢?這里我就不演示了,網(wǎng)上有很多例子,這里只說(shuō)下大致的過(guò)程:
第一:broker-xxx.properties中指定filter個(gè)數(shù)?
第二:上傳一段JAVA代碼,其實(shí)就是一個(gè)類