RocketMQ學習筆記

RocketMQ 作爲阿里開源的一款高性能,高吞吐量的分佈式消息中間件
  • 特點:
    - 支持Broker和Consumer端消息過濾
    - 支持發佈訂閱模型,和點對點
    - 支持pull和push‘兩種消息模式
    - 單一隊列百萬消息,億級消息堆積
    - 支持單master節點,多master節點 ,多master多slave節點
    - 任意一點都是高可用,水平拓展,生產端和消費端,隊列都可以分佈式
    - 消息失敗重試機制,支持特定level的定時消息
    - 新版本底層採用Netty
    - 4.3x版本支持分佈式事務
    - 適合金融類業務,高可用性跟蹤和審計功能

  • 基礎概念
    - Producer:消息生產者
    - Producer Group:消息生產者組,發送同類消息的一個消息生產者組
    - Consumer:消費者
    - Consumer Group:消費同類消息的多個實例
    - Topic:主題,topic是邏輯管理單位,一個topic下可以有多個queue
    - Tag:標籤,子主題,對topic的進一步細化,用於區分同一個主題下的不同業務的消息
    - Message:消息,每個message必須指定一個queue
    - Broker:MQ程序,接受生產的消息,提供給消費者消費的程序
    - NameServer:給生產和消費者提供路由消息,提供輕量級的服務發現,路由,元數據信息,可以多個部署,互相獨立(類似zk,但比zk輕量級)
    - Offset:偏移量,可以理解爲消息進度
    - Commit log:消息存儲會寫在Commit Log文件裏面

  • 心跳機制
    - 單個Broker跟所有Namesrv保持心跳請求,心跳間隔爲30秒,心跳請求中包括當前Broker所有的Topic信息。Namesrv會反查Broer的心跳信息, 如果某個Broker在2分鐘之內都沒有心跳,則認爲該Broker下線,調整Topic跟Broker的對應關係。但此時Namesrv不會主動通知Producer、Consumer有Broker宕機。
    - Consumer跟Broker是長連接,會每隔30秒發心跳信息到Broker。Broker端每10秒檢查一次當前存活的Consumer,若發現某個Consumer 2分鐘內沒有心跳, 就斷開與該Consumer的連接,並且向該消費組的其他實例發送通知,觸發該消費者集羣的負載均衡(rebalance)。
    - 生產者每30秒從Namesrv獲取Topic跟Broker的映射關係,更新到本地內存中。再跟Topic涉及的所有Broker建立長連接,每隔30秒發一次心跳。 在Broker端也會每10秒掃描一次當前註冊的Producer,如果發現某個Producer超過2分鐘都沒有發心跳,則斷開連接。

一個簡單MQ例子:

依賴:

<dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-client</artifactId>
        <version>4.3.0</version>
</dependency>

producer端簡單demo:

@Component
public class MQtestProducer {

    private String producerGroup = "test1_gropup";
    private String nameServerAddr = "127.0.0.1:9876";

    private DefaultMQProducer producer;

    public MQtestProducer(){
        producer = new DefaultMQProducer(producerGroup);

        producer.setNamesrvAddr(nameServerAddr);
        start();
    }

    public void start(){
        try {
            this.producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }

    public DefaultMQProducer getProducer(){
        return this.producer;
    }
    /**
     * 一般在應用上下文,使用上下文監聽器,進行關閉
     */
    public void shutdown(){
        this.producer.shutdown();
    }


}

Consumer端簡單demo:

public class MQtestConsumer {

    private DefaultMQPushConsumer consumer;
    private String consumerGroup = "test_consumer_group";
    private String nameServerAddr = "127.0.0.1:9876";

    private static  final String topic = "mq_test_topic1";


    public MQtestConsumer(){
        try {
        consumer = new DefaultMQPushConsumer(consumerGroup);
        consumer.setNamesrvAddr(nameServerAddr);
        consumer.setMessageModel(MessageModel.CLUSTERING);
        //訂閱
        consumer.subscribe(topic,"*");
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context)->{
            try {
                Message msg = msgs.get(0);
                String topic = msg.getTopic();
                String tags = msg.getTags();
                String body = new String(msg.getBody());
                System.out.println("topic:"+topic+"tags:"+tags+"body:"+body);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }catch (Exception e){
                e.printStackTrace();
                //todo 如果大於三次 就不在消費,消費失敗處理
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }

        });
         consumer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }

    }

}

Controller層:

 @RequestMapping("/api/test1")
    public Object test1() throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
        Message message = new Message(topic,"taga", ("hello wrold rocketmq = ").getBytes() );

        SendResult sendResult = testproducer.getProducer().send(message);

        System.out.println(sendResult);

        return new HashMap<>();
    }

消息隊列生產者核心配置:

  • compressMsgBodyOverHowmuch :消息超過默認字節4096後進行壓縮

  • retryTimesWhenSendFailed : 失敗重發次數

  • maxMessageSize : 最大消息配置,默認128k

  • topicQueueNums : 主題下面的隊列數量,默認是4

  • autoCreateTopicEnable : 是否自動創建主題Topic, 開發建議爲true,生產要爲false

  • defaultTopicQueueNums : 自動創建服務器不存在的topic,默認創建的隊列數

  • autoCreateSubscriptionGroup: 是否允許 Broker 自動創建訂閱組,建議線下開發開啓,線上關閉

  • brokerClusterName : 集羣名稱

  • brokerId : 0表示Master主節點 大於0表示從節點

  • brokerIP1 : Broker服務地址

  • brokerRole : broker角色 ASYNC_MASTER/ SYNC_MASTER/ SLAVE

  • deleteWhen : 每天執行刪除過期文件的時間,默認每天凌晨4點

  • flushDiskType :刷盤策略, 默認爲 ASYNC_FLUSH(異步刷盤), 另外是SYNC_FLUSH(同步刷盤)

  • listenPort : Broker監聽的端口號

  • mapedFileSizeCommitLog : 單個conmmitlog文件大小,默認是1GB

  • mapedFileSizeConsumeQueue:ConsumeQueue每個文件默認存30W條,可以根據項目調整

  • storePathRootDir : 存儲消息以及一些配置信息的根目錄 默認爲用戶的 ${HOME}/store

  • storePathCommitLog:commitlog存儲目錄默認爲${storePathRootDir}/commitlog

  • storePathIndex: 消息索引存儲路徑

  • syncFlushTimeout : 同步刷盤超時時間

  • diskMaxUsedSpaceRatio : 檢測可用的磁盤空間大小,超過後會寫入報錯

消息發送狀態

  • 消息發送有同步和異步
    -同步:SYNC
    應用場景:重要通知郵件,報名短信通知等
    • 異步:ASYNC
      應用場景:註冊成功後通知積分系統發佈優惠卷等
    • 單向發送:ONEWAY 無需等待響應
      應用場景 日誌收集,適用於某些耗時短,但可靠性不高的場景
    • 對比
發送方式 發送tps 發送發送結果反饋反饋 可靠性
同步發送 不丟失
異步發送 不丟失
單向發送 可能丟失
	//同步:
	SendResult sendResult = producer.send(msg);
	
	//異步:
	producer.send(msg, new SendCallback() {
    @Override
      public void onSuccess(SendResult sendResult) {
        //todo 當成功
      }
      @Override
      public void onException(Throwable e) {
         //todo 當異常情況
      }
  });
	
	//單向發送oneway:
	 SendResult sendResult = producer.sendOneway(msg);
  • Broker 消息投遞狀態講解
    - FLUSH_DISK_TIMEOUT
    沒有在規定時間完成刷盤會出現這個錯誤
    - FLUSH_SLAVE_AVALABLE
    從模式下,broker是SYNC_MASTER,但是沒有找到被配置成Slave的Broker
    - SEND_OK
    發送成功,沒有發生上面的三種問題

消息重試及處理

  • producer重試:
    - 消息重投
    - 如果網絡比較差改多幾次

  • consumer重試:
    - 原因:消息處理異常,broker端到consumer端問題,網絡原因,消費處理失敗等
    - 注:重試間隔時間配置,默認每條消息最多重試16次

      messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
    

    -超過重試次數人工補償
    -一條消息的MessageID和key一直不變
    -消費重試指針對集羣消費方式生效:廣播方式不提供失敗重試特性,即消費失敗後,失敗消息不再重試,繼續消費新消息

 consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context)->{
            try {
                Message msg = msgs.get(0);
                String topic = msg.getTopic();
                String tags = msg.getTags();
                String body = new String(msg.getBody());
                System.out.println("topic:"+topic+"tags:"+tags+"body:"+body);
                if(true)throw  new Exception();
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }catch (Exception e){
                e.printStackTrace();
                //todo 如果大於三次 就不在消費
                return ConsumeConcurrentlyStatus.RECONSUME_LATER;
            }

        });
         consumer.start();	

順序消息

  • 什麼是順序消息: 消費的生產 和消費順序一致,
  • 順序發佈:對於指定的一個Topic,客戶端按照一定的先後順序發送消息
  • 順序消費:對於指定的一個Topic,按照一定的先後順序接受消息,即先發送的消息一定會先被客戶端收到
  • 順序消息不支持異步的發送方式,不支持廣播模式

生產者保證發送消息有序,且發送到同一個Topic的同個queue裏面,可以根據MessageQueueSelector裏面自定義策略,根據同個業務id放置到同個queue裏賣弄,如訂單號通過取模運算再放到selecto中,同一個模的值都會投遞到同一個queue

public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {  
	Long id = (Long) arg;     
	long index = id % mqs.size();      
	return mqs.get((int)index);   
}
  • 消費端要在保證消費同個topic裏的同個隊列,不應該用MessageListenerConcurrently,應該使用MessageListenerOrderly,自帶單線程消費消息,不能在Consumer端再使用更多線程去消費,消費端分配到的queue數量是固定的,集羣會所鎖住當前消費的隊列集合的消息,所以保證順序消費

  • MessageListenerOrderly
    Conusmer會平均分配queue數量,並不是簡單禁止併發處理,而是每個ConsumerQueue加個鎖,消費每個消費錢,需要獲得這個消息所在的Queue的鎖,

消息隊列消費者核心配置

  • consumerFromWhere配置
    - CONSUME_FROM_FIRSR_OFFSET: 初次從消息隊列頭部開始消費,即歷史消息全部消費一邊,後續再啓動接着上次笑得的進度開始消費
    - CONSUME_FROM_LAST_OFFSET:默認策略,初次從該隊列尾開始消費,即跳過歷史消息,後續再啓動接着上次消費的進度開始消費
    - CONSUME_FROM_TIMESTAMP:從某個時間點開始消費,默認是半個小時以前,後續啓動接着上次消費的嫉妒開始消費
  • allocateMessageQueueStratrgy:
    - 負載均衡策略算法,即消費者分配到queue的算法,默認值是AllocateMEssageQueueAveragely即取模平均分配
    - LocalFileOffsetStore和RemoteBrokerOffsetStor 廣播模式默認使用LocalFIleOffsetStore集羣模式默認是用RemoteBrokerOffsetStore
  • consumerThreadMin:最小消費線程池數量
  • consumeThreadMax:最大消費線程池數量
  • pullBatchSize:消費者去broker拉去消息時,一次拉取多少條消息,可選配置
  • consumeMessageBatchMaxSize:單次消費時一次性消費多少條消息,批量消費接口才有用,可選配置
  • messageModel:消費模式,默認集羣模式–CLUSTERING 廣播模式-BROADCASRING

PullConsumer模式和PushConsumer模式

模式 優缺點
push 實時性高,但消費端能力不同,如果push推送過快,消費端會出現很多問題
pull 消費者從broker拉取,主動權在消費端,可控性好,但是間隔時間不好設置,間隔太短,則空請求,間隔太長,則消息不能及時處理
  • 長輪詢:Client請求broker時,broker會保持當前連接一段時間,默認15s,如果這段時間有消息到達,則立刻返回consumer,沒有消息的話,超過15s,則返回空,再進行重新請求,主動權再Consumer,broker即使有大量的雄安錫,也不會主動提送Consumer,缺點則是broker要保持consumer的請求,佔用資源

消息偏移量Offset

  • message queue是無限長得數組,一條消息進來下標就會漲1,下標就是offset,消息在某個MessageQueue裏的位置,通過offset的值可以定位到這條消息,或者指示Consumer從這條消息開始向後處理
  • Message Queue中的MaxOffset表示消息的最大offset,maxoffset並不是最新的那條消息的offset,而是最新消息的offset+1,minoffset則是現存在的最小offset
  • 作用:主要是用來記錄消息的偏移量,集羣模式下采用RemoteBrokerOffsetStore,broker控制offset的值。廣播模式下采用LocalFileOffsetStore,消費端存儲

CommitLog

  • CommitLog是消息文件的存儲地址,每個文件默認大小爲1G
  • ConsumeQueue是邏輯隊列,而CommitLog是真正的存儲消息文件的,存儲的是指向物理存儲的地址
  • Broker裏面有一個Topic,一個Topic裏面有多個MessageQueue,而一個MessageQueue對應一個ConsumeQueue,ConsumeQueue裏面記錄的是消息再CommitLog裏面的物理存儲地址
  • 默認地址/store/consumequeue/{topicName}/fileName

RocketMQ高效原因–>ZeroCopy

  • RocketMQ高效的原因:
    • CommitLog順序寫,存儲了MessageBody,message key,tag等i西南西
    • ConsumeQueue隨機讀+操作系統的PageCache + 零拷貝技術ZeroCopy
      將文件讀取併發送傳統做法:
      調用read,將文件拷貝到kernel內核態,cpu控制內核太的數據copy到用戶態,然後寫的時候,用戶態的數據先copy到socket的buffer中,最後將socket buffer的數據copy到網卡設備中,如圖:

傳統做法
-ZeroCopy 請求kernel直接把硬盤讀出的數據傳輸給socket,而不是再經過應用程序,zerocopy提高了性能,減少了不必要的內核緩衝區跟用戶緩衝區間的拷貝,從而減少了cpu的開銷,kernel和user模式的上下文切換,達到性能的提升
ZeroCopy

  • RocketMQ分佈式事務

分佈式事務流程
1 Producer向broker端發送消息
2 服務端接受到消息後,將消息持久化後,向發送方ack確認消息已經發送成功,此時消息是半消息狀態,
3 發送方開始執行本地事務邏輯
4 發送方根據本地事務執行結果向服務端提交二次確認(commit或者rollback),服務端收到conmit狀態則將半消息標記爲可投遞,訂閱方最終收到該消息,服務端收到rollback狀態則刪除半消息,訂閱方則不會接受該消息
5 斷網或者應用重啓得特殊狀態下,狀態4提交得二次確認狀態可能丟失,經過固定時間後,broker會對該消息進行回查
6 發送方收到消息回查後,需要檢查對應消息得本地事務執行得最終狀態
7 發送方根據檢查得到的本地事務的最終狀態再次提交二次確認,服務端仍然按照步驟4對半消息進行操作

  • 半消息
    暫時不能投遞的消息,producer已經將消息成功發送到broker端,但是broker沒有收到producer的二次確認,此消息被標記爲暫時不能投遞狀態,即爲半消息
  • 消息狀態
    COMMIT_MESSAGE:提交事務消息,消費者可以消費此消息
    ROLLBACK_MESSAGE:回滾事務消息
    UNKOW:broker需要回查確認消息的狀態
    –未完待續
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章