RabbitMQ都寫了,RocketMQ怎么能落下?

整體架構

最近看到了我在Github上寫的rabbitmq-examples陸續被人star了,就想著寫個rocketmq-examples。對rabbitmq感興趣的小伙伴可以看我之前的文章。下面把RocketMQ的各個特性簡單介紹一下,這樣在用的時候心里也更有把握

RocketMQ是阿里自研的消息中間件,RocketMQ的整體架構如下

RabbitMQ都寫了,RocketMQ怎么能落下?

主要有4個角色

Producer:消息生產者。類似,發信者 Consumer:消息消費者。類似,收信者 BrokerServer:消息的存儲,投遞,查詢。類似,郵局 NameServer:注冊中心,支持Broker的動態注冊與發現。類似,郵局的管理結構

再介紹幾個基本概念

Topic(主題):一類消息的集合,Topic和消息是一對多的關系。每個Broker可以存儲多個Topic的消息,每個Topic也可以分片存儲于不同的Broker

Tag(標簽):在Topic類別下的二級子類別。如財務系統的所有消息的Topic為Finance_Topic,創建訂單消息的Tag為Create_Tag,關閉訂單消息的Tag為Close_Tag。這樣就能根據Tag消費不同的消息,當然你也可以為創建訂單和關閉訂單的消息各自創建一個Topic

Message Queue(消息隊列):相當于Topic的分區,用于并行發送和消費消息。Message Queue在Broker上,一個Topic默認的Message Queue的數量為4

Producer Group(生產者組):同一類Producer的集合。如果發送的是事務消息且原始生產者在發送之后崩潰,Broker會聯系統一生產者組內的其他生產者實例以提交或回溯消費

Consumer Group(消費者組):同一類Consumer的集合。消費者組內的實例必須訂閱完全相同的Topic

Clustering(集群消費):相同Consumer Group下的每個Consumer實例平均分攤消息

Broadcasting(廣播消費):相同Consumer Group的每個Consumer實例都接收全量的消息

用圖演示一下Clustering和Broadcasting的區別

RabbitMQ都寫了,RocketMQ怎么能落下?

如果我有一條訂單程成交的消息,財務系統和物流系統都要同時訂閱消費這條消息,該怎么辦呢?定義2個Consumer Group即可

RabbitMQ都寫了,RocketMQ怎么能落下?

Consumer1和Consumer2屬于一個Consumer Group,Consumer3和Consumer4屬于一個Consumer Group,消息會全量發送到這2個Consuemr Group,至于這2個Consumer Group是集群消費還是廣播消費,自己定義即可

工作流程在官方文檔寫的很詳細,不再深入了

https://github.com/apache/rocketmq/tree/master/docs/cn

RabbitMQ都寫了,RocketMQ怎么能落下?

Message

消息的各種處理方式涉及到的內容較多,所以我就不在文章中放代碼了,直接放GitHub了,目前還在不斷完善中

地址為:https://github.com/erlieStar/rocketmq-examples,

和之前的RabbitMQ一個風格,基本上所有知識點都涉及到了

地址為:https://github.com/erlieStar/rabbitmq-example

RabbitMQ都寫了,RocketMQ怎么能落下?

每個消息必須屬于一個Topic。RocketMQ中每個消息具有唯一的Message Id,且可以攜帶具有業務標識的Key,我們可以通過Topic,Message Id或Key來查詢消息

消息消費的方式

  1. Pull(拉取式消費),Consumer主動從Broker拉取消息
  2. Push(推送式消費),Broker收到數據后會主動推送給Consumer,實時性較高

消息的過濾方式

  1. 指定Tag
  2. SQL92語法過濾

消息的發送方式

  1. 同步,收到響應后才會發送下一條消息
  2. 異步,一直發,用異步的回調函數來獲取結果
  3. 單向(只管發,不管結果)

消息的種類

  1. 順序消息
  2. 延遲消息
  3. 批量消息
  4. 事務消息

順序消息

順序消息分為局部有序和全局有序

官方介紹為普通順序消息和嚴格順序消息

局部有序:同一個業務相關的消息是有序的,如針對同一個訂單的創建和付款消息是有序的,只需要在發送的時候指定message queue即可,如下所示,將同一個orderId對應的消息發送到同一個隊列

SendResult sendResult = producer.send( message, new MessageQueueSelector()
                       {
                                                /**
                                                 * @param mqs topic對應的message queue
                                                 * @param msg send方法傳入的message
                                                 * @param arg send方法傳入的orderId
                                                 */
                           @Override
                           public MessageQueue select( List<MessageQueue> mqs, Message msg, Object arg )
                           {
                                                /* 根據業務對象選擇對應的隊列 */
                               Integer orderId = (Integer) arg;
                               int index = orderId % mqs.size();
                               return(mqs.get( index ) );
                           }
                       }, orderId );

消費者所使用的Listener必須是MessageListenerOrderly(對于一個隊列的消息采用一個線程去處理),而平常的話我們使用的是MessageListenerConcurrently

全局有序:要想實現全局有序,則Topic只能有一個message queue。

延遲消息

RocketMQ并不支持任意時間的延遲,需要設置幾個固定的延時等級,從1s到2h分別對應著等級1到18

/* org.apache.rocketmq.store.config.MessageStoreConfig */
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"

批量消息

批量發送消息能顯著提高傳遞小消息的性能,限制是這批消息應該有相同的topic,相同的waitStoreMsgOK,而且不能是延時消息,一批消息的總大小不應超過1MB

事務消息

事務在實際的業務場景中還是經常遇到的,以轉賬為例子

張三給李四轉賬100元,可以分為如下2步

  1. 張三的賬戶減去100元
  2. 李四的賬戶加上100元

這2個操作要是同時成功,要是同時失敗,不然會造成數據不一致的情況,基于單個數據庫Connection時,我們只需要在方法上加上@Transactional注解就可以了。

如果基于多個Connection(如服務拆分,數據庫分庫分表),加@Transactional此時就不管用了,就得用到分布式事務

分布式事務的解決方案很多,RocketMQ只是其中一種方案,RocketMQ可以保證最終一致性

RabbitMQ都寫了,RocketMQ怎么能落下?

RocketMQ實現分布式事務的流程如下

  1. producer向mq server發送一個半消息
  2. mq server將消息持久化成功后,向發送方確認消息已經發送成功,此時消息并不會被consumer消費
  3. producer開始執行本地事務邏輯
  4. producer根據本地事務執行結果向mq server發送二次確認,mq收到commit狀態,將消息標記為可投遞,consumer會消費該消息。mq收到rollback則刪除半消息,consumer將不會消費該消息,如果收到unknow狀態,mq會對消息發起回查
  5. 在斷網或者應用重啟等特殊情況下,步驟4提交的2次確認有可能沒有到達mq server,經過固定時間后mq會對該消息發起回查
  6. producer收到回查后,需要檢查本地事務的執行狀態
  7. producer根據本地事務的最終狀態,再次提交二次確認,mq仍按照步驟4對半消息進行操作

理解了原理,看代碼實現就很容易了,放一個官方的example

public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger index = new AtomicInteger( 0 );

    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    @Override
    public LocalTransactionState executeLocalTransaction( Message msg, Object arg )
    {
        int value   = index.getAndIncrement();
        int status  = value % 3;
        localTrans.put( msg.getTransactionId(), status );
        return(LocalTransactionState.UNKNOW);
    }


    @Override
    public LocalTransactionState checkLocalTransaction( MessageExt msg )
    {
        Integer status = localTrans.get( msg.getTransactionId() );
        if ( status != null )
        {
            switch ( status )
            {
            case 0:
                return(LocalTransactionState.UNKNOW);
            case 1:
                return(LocalTransactionState.COMMIT_MESSAGE);
            case 2:
                return(LocalTransactionState.ROLLBACK_MESSAGE);
            default:
                return(LocalTransactionState.COMMIT_MESSAGE);
            }
        }
        return(LocalTransactionState.COMMIT_MESSAGE);
    }
}

實現分布式事務需要實現TransactionListener接口,2個方法的作用如下

  1. executeLocalTransaction,執行本地事務
  2. checkLocalTransaction,回查本地事務狀態

針對這個例子,所有的消息都會回查,因為返回的都是UNKNOW,回查的時候status=1的數據會被消費,status=2的數據會被刪除,status=0的數據會一直回查,直到超過默認的回查次數。

發送方代碼如下

public class TransactionProducer {
    public static final String  RPODUCER_GROUP_NAME = "transactionProducerGroup";
    public static final String  TOPIC_NAME      = "transactionTopic";
    public static final String  TAG_NAME        = "transactionTag";

    public static void main( String[] args ) throws Exception
    {
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer   producer        = new TransactionMQProducer( RPODUCER_GROUP_NAME );

        ExecutorService executorService = new ThreadPoolExecutor( 2, 5, 100, TimeUnit.SECONDS,
                                      new ArrayBlockingQueue<>( 100 ), new ThreadFactory()
                                      {
                                          @Override
                                          public Thread newThread( Runnable r )
                                          {
                                              Thread thread = new Thread();
                                              thread.setName( "transaction-msg-check-thread" );
                                              return(thread);
                                          }
                                      } );
        producer.setExecutorService( executorService );
        producer.setTransactionListener( transactionListener );
        producer.start();

        for ( int i = 0; i < 100; i++ )
        {
            Message message = new Message( TOPIC_NAME, TAG_NAME,
                               ("hello rocketmq " + i).getBytes( RemotingHelper.DEFAULT_CHARSET ) );
            SendResult sendResult = producer.send( message );
            System.out.println( sendResult );
        }

        TimeUnit.HOURS.sleep( 1 );
        producer.shutdown();
    }
}

看到這,可能有人會問了,我們先執行本地事務,執行成功后再發送消息,這樣可以嗎?

其實這樣做還是有可能會造成數據不一致的問題。假如本地事務執行成功,發送消息,由于網絡延遲,消息發送成功,但是回復超時了,拋出異常,本地事務回滾。但是消息其實投遞成功并被消費了,此時就會造成數據不一致的情況

那消息投遞到mq server,consumer消費失敗怎么辦?

如果是消費超時,重試即可。如果是由于代碼等原因真的消費失敗了,此時就得人工介入,重新手動發送消息,達到最終一致性。

消息重試

發送端重試

producer向broker發送消息后,沒有收到broker的ack時,rocketmq會自動重試。重試的次數可以設置,默認為2次

DefaultMQProducer producer = new DefaultMQProducer( RPODUCER_GROUP_NAME );
/* 同步發送設置重試次數為5次 */
producer.setRetryTimesWhenSendFailed( 5 );
/* 異步發送設置重試次數為5次 */
producer.setRetryTimesWhenSendAsyncFailed( 5 );

消費端重試

順序消息的重試

對于順序消息,當Consumer消費消息失敗后,RocketMQ會不斷進行消息重試,此時后續消息會被阻塞。所以當使用順序消息的時候,監控一定要做好,避免后續消息被阻塞

無序消息的重試

當消費模式為集群模式時,Broker才會自動進行重試,對于廣播消息是不會進行重試的

當consumer消費消息后返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS表明消費消息成功,不會進行重試

當consumer符合如下三種場景之一時,會對消息進行重試

  1. 返回ConsumeConcurrentlyStatus.RECONSUME_LATER
  2. 返回null
  3. 主動或被動拋出異常

RocketMQ默認每條消息會被重試16次,超過16次則不再重試,會將消息放到死信隊列,當然我們也可以自己設置重試次數

每次重試的時間間隔如下

RabbitMQ都寫了,RocketMQ怎么能落下?

重試隊列和死信隊列

當消息消費失敗,會被發送到重試隊列

當消息消費失敗,并達到最大重試次數,rocketmq并不會將消息丟棄,而是將消息發送到死信隊列

死信隊列有如下特點

  1. 里面存的是不能被正常消費的消息
  2. 有效期與正常消息相同,都是3天,3天后會被刪除
  3. 每個死信隊列對應一個Consumer Group ID,即死信隊列是消費者組級別的
  4. 如果一個Consumer Group沒有產生死信消息,則RocketMQ不會創建對應的死信隊列
  5. 死信隊列包含了一個Consumer Group下的所有死信消息,不管該消息屬于哪個Topic

重試隊列的命名為 %RETRY%消費組名稱 死信隊列的命名為 %DLQ%消費組名稱

RocketMQ高性能和高可用的方式

整體架構

RabbitMQ都寫了,RocketMQ怎么能落下?

rocketmq是通過broker主從機制來實現高可用的。相同broker名稱,不同brokerid的機器組成一個broker組,brokerId=0表明這個broker是master,brokerId>0表明這個broker是slave。

消息生產的高可用:創建topic時,把topic的多個message queue創建在多個broker組上。這樣當一個broker組的master不可用后,producer仍然可以給其他組的master發送消息。rocketmq目前還不支持主從切換,需要手動切換

消息消費的高可用:consumer并不能配置從master讀還是slave讀。當master不可用或者繁忙的時候consumer會被自動切換到從slave讀。這樣當master出現故障后,consumer仍然可以從slave讀,保證了消息消費的高可用

消息存儲結構

RocketMQ需要保證消息的高可靠性,所以要將數據通過磁盤進行持久化存儲。

將數據存到磁盤會不會很慢?其實磁盤有時候比你想象的快,有時候比你想象的慢。目前高性能磁盤的順序寫速度可以達到600M/s,而磁盤的隨機寫大概只有100k/s,和順序寫的性能相差6000倍,所以RocketMQ采用順序寫。

并且通過mmap(零拷貝的一種實現方式,零拷貝可以省去用戶態到內核態的數據拷貝,提高速度)具體原理并不是很懂,有興趣的小伙伴可以看看相關書籍

總而言之,RocketMQ通過順序寫和零拷貝技術實現了高性能的消息存儲

RabbitMQ都寫了,RocketMQ怎么能落下?

和消息相關的文件有如下幾種

  1. CommitLog:存儲消息的元數據
  2. ConsumerQueue:存儲消息在CommitLog的索引
  3. IndexFile:提供了一種通過key或者時間區間來查詢消息的方法

刷盤機制

RabbitMQ都寫了,RocketMQ怎么能落下?
  1. 同步刷盤:消息被寫入內存的PAGECACHE,返回寫成功狀態,當內存里的消息量積累到一定程度時,統一觸發寫磁盤操作,快速寫入 。吞吐量低,但不會造成消息丟失
  2. 異步刷盤:消息寫入內存的PAGECACHE后,立刻通知刷盤線程刷盤,然后等待刷盤完成,刷盤線程執行完成后喚醒等待的線程,給應用返回消息寫成功的狀態。吞吐量高,當磁盤損壞時,會丟失消息

主從復制

如果一個broker有master和slave時,就需要將master上的消息復制到slave上,復制的方式有兩種

  1. 同步復制:master和slave均寫成功,才返回客戶端成功。maste掛了以后可以保證數據不丟失,但是同步復制會增加數據寫入延遲,降低吞吐量
  2. 異步復制:master寫成功,返回客戶端成功。擁有較低的延遲和較高的吞吐量,但是當master出現故障后,有可能造成數據丟失

負載均衡

Producer負載均衡

producer在發送消息時,默認輪詢所有queue,消息就會被發送到不同的queue上。而queue可以分布在不同的broker上

RabbitMQ都寫了,RocketMQ怎么能落下?

Consumer負載均衡

默認的分配算法是AllocateMessageQueueAveragely,如下圖

RabbitMQ都寫了,RocketMQ怎么能落下?

還有另外一種平均的算法是AllocateMessageQueueAveragelyByCircle,也是平均分攤queue,只是以環狀輪流分queue的形式,如下圖:

RabbitMQ都寫了,RocketMQ怎么能落下?

如果consumer數量比message queue還多,則多會來的consumer會被閑置。所以不要讓consumer的數量多于message queue的數量

圖形化管理工具

在rocketmq-externals這個項目中提供了rocketmq的很多擴展工具

github地址如下:https://github.com/apache/rocketmq-externals

RabbitMQ都寫了,RocketMQ怎么能落下?

其中有一個子項目rocketmq-console提供了rocketmq的圖像化工具,提供了很多實用的功能,如前面說的通過Topic,Message Id或Key來查詢消息,重新發送消息等,還是很方便的

RabbitMQ都寫了,RocketMQ怎么能落下?
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374