RocketMQ 場景及使用

一、整體介紹

  1. RocketMQ 是一款分佈式、隊列模型的消息中間件
  2. 支持分佈式事務
  3. 天然的支持集羣模型、負載均衡、水平擴展能力
  4. 億級別的消息堆積能力
  5. 採用零拷貝的原理, 循序寫盤,隨機讀
  6. 底層通信框架採用netty NIO 框架
  7. NameServer 代替Zookeeper ,更輕量級
  8. 消息失敗重試機制,消息可查詢(可設置時間間隔和重試次數)

應用場景

  • 削峯填谷:諸如秒殺、搶紅包、企業開門紅等大型活動時皆會帶來較高的流量脈衝,或因沒做相應的保護而導致系統超負荷甚至崩潰,或因限制太過導致請求大量失敗而影響用戶體驗,消息隊列 MQ 可提供削峯填谷的服務來解決該問題。
  • 異步解耦:交易系統作爲淘寶/天貓主站最核心的系統,每筆交易訂單數據的產生會引起幾百個下游業務系統的關注,包括物流、購物車、積分、流計算分析等等,整體業務系統龐大而且複雜,消息隊列 MQ 可實現異步通信和應用解耦,確保主站業務的連續性。
  • 順序收發: 細數日常中需要保證順序的應用場景非常多,比如證券交易過程時間優先原則,交易系統中的訂單創建、支付、退款等流程,航班中的旅客登機消息處理等等。與先進先出(First In First Out,縮寫 FIFO)原理類似,消息隊列 MQ 提供的順序消息即保證消息 FIFO。
  • 分佈式事務一致性:交易系統、支付紅包等場景需要確保數據的最終一致性,大量引入消息隊列 MQ 的分佈式事務,既可以實現系統之間的解耦,又可以保證最終的數據一致性。
  • 大數據分析: 數據在“流動”中產生價值,傳統數據分析大多是基於批量計算模型,而無法做到實時的數據分析,利用阿里雲消息隊列 MQ 與流式計算引擎相結合,可以很方便的實現將業務數據進行實時分析。
  • 分佈式緩存同步: 天貓雙 11 大促,各個分會場琳琅滿目的商品需要實時感知價格變化,大量併發訪問數據庫導致會場頁面響應時間長,集中式緩存因爲帶寬瓶頸限制商品變更的訪問流量,通過消息隊列 MQ 構建分佈式緩存,實時通知商品數據的變化。
    更多使用場景參考 -> 阿里使用場景

二、 RocketMQ 安裝

Git地址:https://github.com/apache/rocketmq/tree/release-4.3.0

1、 在解壓後的文件夾中執行maven命令,獲取程序運行包(生成的包在rocketmq-distribution/target路徑下):

mvn -Prelease-all -DskipTests clean install -U

2、rocketmq是一個集羣模型的消息隊列,這裏我們用兩臺服務器來部署rocketmq,爲了方便和區分,分別把兩臺服務器標註一下角色,如下節點配置:

IP 角色 模式
106.53.92.xxx nameServer1,brokerServer1 Master1
47.105.189.xx nameServer2,brokerServer2 Master2

3、創建消息隊列信息保存路徑

/usr/local/rocketmq/store/index
/usr/local/rocketmq/store/commitlog
/usr/local/rocketmq/store/consumequeue

4、修改broker配置


brokerClusterName=rocketmq-cluster
# broker 名字,不同文件命名不一樣
brokerName=broker-a
# 0表示Master >0 表示slave
brokerId=0
brokerIP1=本機IP
# nameServer地址,多個使用分號分割
namesrvAddr=rocketmq-nameserver1:9876
# 默認創建的隊列數
defaultTopicQueueNums=4
# 是否允許broker 自動創建topic 
autoCreateTopicEnable=true
# 是否允許broker自動創建訂閱組
autoCreateSubscriptionGroup=true
# Broker 對外服務的監聽端口
listenPort=10911
# 刪除文件時間點,默認凌晨4點
deleteWhen=04
# 文件保留時間,默認48小時
fileReservedTime=120
# commitLog每個文件的大小默認1G
mapedFileSizeCommitLog=1073741824
# consumQueue每個文件默認存30W條
mapedFileSizeConsumeQueue=300000
# 檢測物理文件磁盤空間
diskMaxUsedSpaceRatio=88
#存儲路徑 
storePathRootDir=/usr/local/rocketmq/store
# commitLog存儲路徑 
storePathCommitLog=/usr/local/rocketmq/store/commitlog
# 消費隊列存儲路徑 
storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue
# 消息索引存儲路徑 
storePathIndex=/usr/local/rocketmq/store/index
# checkpoint 文件存儲路徑 
storeCheckpoint=/usr/local/rocketmq/store/checkpoint
# abort 文件存儲路徑 
abortFile=/usr/local/rocketmq/store/abort
# ASYNC_MASTER 異步賦值 SYNC_MASTER 同步刷盤  SLAVE
brokerRole=ASYNC_MASTER
# 刷盤方式 ASYNC_FLUSH 異步刷盤   SYNC_FLUSH同步刷盤
flushDiskType=ASYNC_FLUSH

如果配置主從模式,則需要修改broke-a-s.properties

# 0表示Master >0 表示slave
brokerId=1
brokerIP1=本機IP
# ASYNC_MASTER 異步賦值 SYNC_MASTER 同步刷盤  SLAVE
brokerRole=SLAVE

5、 修改bin 下的runbroker.sh和 runserver.sh

JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn1g"

6、運行

// 啓動nameserver
nohup sh ./mqnamesrv &
// 啓動broker
nohup sh ./mqbroker -c ../conf/2m-2s-async/broker-a.properties > /dev/null 2&>1 &
// 關閉broker
sh mqshutdown broker
// 關閉nameServer
sh mqshutdown namesrv

可以用netstat -ntlp 查看一下端口占用情況

6、RocketMQ-Console(Git地址)

進入console模塊,修改application.properties

rocketmq.config.namesrvAddr=106.53.92.208:9876
	// 在pom目錄下打包
    $ mvn clean package -Dmaven.test.skip=true
    // 運行
    $ java -jar target/rocketmq-console-ng-1.0.0.jar
    // 如果配置文件沒有填寫Name Server
    $ java -jar target/rocketmq-console-ng-1.0.0.jar --rocketmq.config.namesrvAddr='10.0.74.198:9876;10.0.74.199:9876'

三、 代碼部分

1、一個簡單的實例

消息生產者

public class Producer {
    private static final Logger logger = LoggerFactory.getLogger("Customer");
    
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("rocketmq-cluster");
        // 設置nameserver 地址
        producer.setNamesrvAddr(MQConstant.NAMESERVER1);
        // 啓動實例
        producer.start();
        for (int i = 0; i < 10; i++) {
            // 加入TagA TagB 兩個標籤用於測試
            String tag = "TagA";
            if(i%2==0){ tag = "TagB"; }
            // 創建消息
            Message msg = new Message("test", tag ,"key"+i,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            // 發送消息
            SendResult sendResult = producer.send(msg);
            logger.info(JSONObject.toJSONString(sendResult));
        }
        // 一旦生產者實例不再使用,則關閉該實例
        producer.shutdown();
    }
}

消息消費者

public class Customer {
    private static final Logger logger = LoggerFactory.getLogger("Customer");
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-cluster");
        // nameserver 地址
        consumer.setNamesrvAddr(MQConstant.NAMESERVER1);
        // 設置消息offset 位置
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        // 訂閱消息主題
        consumer.subscribe("test","*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt me = list.get(0);
                //for (MessageExt me : list){
                try {
                    String topic = me.getTopic();
                    String tags = me.getTags();
                    String keys = me.getKeys();
                    String msgBody = "";
                    msgBody = new String(me.getBody(),"utf-8");
                    logger.info("topic:{}, tags:{}, keys:{} {}",topic,tags,keys,msgBody);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                //}
                return  ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消費者已啓動");
    }
}

消費者中, 如果消息第一次消費失敗怎麼辦?
customer 中可以添加消息重試機制,當消息第一次失敗可以可以進行重試,代碼如下

public class Customer {

    private static final Logger logger = LoggerFactory.getLogger("Customer");

    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-cluster");
        // nameserver 地址
        consumer.setNamesrvAddr(MQConstant.NameServerAndSlave);
        // 設置消息offset 位置
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        // 訂閱消息主題
        consumer.subscribe("test","*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt me = list.get(0);
                //for (MessageExt me : list){
                try {
                    String topic = me.getTopic();
                    String tags = me.getTags();
                    String keys = me.getKeys();
                    String msgBody = "";
                    msgBody = new String(me.getBody(),"utf-8");
                    int i = 2/0;
                    logger.info("topic:{}, tags:{}, keys:{} {}",topic,tags,keys,msgBody);
                } catch (Exception e) {
                    e.printStackTrace();
                    int reconsumeTimes = me.getReconsumeTimes();
                    logger.info("重試次數:{}",reconsumeTimes);
                    if(reconsumeTimes==3){
                        logger.error("日誌補償。。。");
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                    }
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                //}
                return  ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消費者已啓動");
    }
}

2、生產者

2.1、延遲消息

  • 延遲消息,消息發送到broker後,要特定的時間纔會被Consumer消費
  • 目前只支持固定精度的定時消息
  • msg.setDelayTimeLevel(2); 方法設置

2.2、發送消息到指定隊列

	 // 發送到指定的隊列中去
      SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
          @Override
          public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
              Integer queueNum = (Integer) o;
              return list.get(queueNum);
          }
      }, 1);

3、生產者

3.1 PushConsumer 消費模式 - 集羣模式

  • Clustering 模式 (默認)
  • GroupName 用於把多個Comsummer組織到一起
  • 相同Groupname 的Consumer 只消費所訂閱消息的一部分
  • 目的:達到天然的負載均衡機制

適用場景
適用於消費端集羣化部署,每條消息只需要被處理一次的場景。此外,由於消費進度在服務端維護,可靠性更高。具體消費示例如下圖所示。
在這裏插入圖片描述
注意事項
集羣消費模式下,每一條消息都只會被分發到一臺機器上處理。如果需要被集羣下的每一臺機器都處理,請使用廣播模式。
集羣消費模式下,不保證每一次失敗重投的消息路由到同一臺機器上。

當我們啓動兩個消費之 A, B,然後再生產10條消息, 這時我們可以看到A消費了6條,B消費了4條,爲啥會這樣,不能實現負載均衡嗎?
因爲消費者通過監聽消息對列來實現消息的接受, 加入四個消息對列, 生產者分別向兩個消息隊列投了三條消息,另外兩個投了兩條,則會出現這種情況。

21:47:04.981 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3490000","offsetMsgId":"6A355CD000002A9F000000000002045E","queueOffset":40,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.028 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3950001","offsetMsgId":"6A355CD000002A9F0000000000020515","queueOffset":60,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.065 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":0,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3C40002","offsetMsgId":"6A355CD000002A9F00000000000205CC","queueOffset":39,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.112 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":1,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3E90003","offsetMsgId":"6A355CD000002A9F0000000000020683","queueOffset":64,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.149 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4180004","offsetMsgId":"6A355CD000002A9F000000000002073A","queueOffset":41,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.185 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B43E0005","offsetMsgId":"6A355CD000002A9F00000000000207F1","queueOffset":61,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.219 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":0,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4610006","offsetMsgId":"6A355CD000002A9F00000000000208A8","queueOffset":40,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.259 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":1,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4830007","offsetMsgId":"6A355CD000002A9F000000000002095F","queueOffset":65,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.298 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4AB0008","offsetMsgId":"6A355CD000002A9F0000000000020A16","queueOffset":42,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.338 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4D20009","offsetMsgId":"6A355CD000002A9F0000000000020ACD","queueOffset":62,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}

如果存在這樣一個場景, 一個producer 的同一個topic下分別有TagA、TagB、TagC , 這三個tag 分別需要被三個不同的consumer 消費,這樣該如何實現?
我們所知的pushConsumer的集羣模式不能很好地去實現這種需求,會有其他辦法嗎?

3.2 PushConsumer 消費模式 - 廣播模式

  • BroadCasting 模式 (廣播模式)

  • 同一個ConsumerGroup 裏的Consumer 都消費訂閱topic 全部信息,也就是一條消息會被每一個Consumer消費

  • consumer.setMessageModel(MessageModel.BROADCASTING);方法設置廣播模式

廣播消費:當使用廣播消費模式時,消息隊列 MQ 會將每條消息推送給集羣內所有註冊過的消費者,保證消息至少被每個消費者消費一次。
在這裏插入圖片描述
注意事項

  • 廣播消費模式下不支持順序消息。
  • 廣播消費模式下不支持重置消費位點。
  • 每條消息都需要被相同訂閱邏輯的多臺機器處理。
  • 消費進度在客戶端維護,出現重複消費的概率稍大於集羣模式。
  • 廣播模式下,消息隊列 MQ 保證每條消息至少被每臺客戶端消費一次,但是並不會重投消費失敗的消息,因此業務方需要關注消費失敗的情況。
  • 廣播模式下,客戶端每一次重啓都會從最新消息消費。客戶端在被停止期間發送至服務端的消息將會被自動跳過,請謹慎選擇。
  • 廣播模式下,每條消息都會被大量的客戶端重複處理,因此推薦儘可能使用集羣模式。
  • 廣播模式下服務端不維護消費進度,所以消息隊列 MQ 控制檯不支持消息堆積查詢、消息堆積報警和訂閱關係查詢功能。

通過上述可以看到廣播模式並不推薦使用,我們可以通過集羣模式來模擬廣播模式

使用集羣模式模擬廣播
適用場景:
適用於每條消息都需要被多臺機器處理,每臺機器的邏輯可以相同也可以不一樣的場景。具體消費示例如下圖所示。
在這裏插入圖片描述
如果業務需要使用廣播模式,也可以創建多個 Group ID,用於訂閱同一個 Topic。

注意事項
消費進度在服務端維護,可靠性高於廣播模式。
對於一個 Group ID 來說,可以部署一個消費者實例,也可以部署多個消費者實例。當部署多個消費者實例時,實例之間又組成了集羣模式(共同分擔消費消息)。假設 Group ID 1 部署了三個消費者實例 C1、C2、C3,那麼這三個實例將共同分擔服務器發送給 Group ID 1 的消息。同時,實例之間訂閱關係必須保持一致。

3.3 PullConsumer 消息拉取消費模式

pull方式裏,取消息的過程需要用戶自己寫,首先通過打算消費的Topic拿到MessageQueue的集合,遍歷MessageQueue集合,然後針對每個MessageQueue批量取消息,一次取完後,記錄該隊列下一次要取的開始offset,直到取完了,再換另一個MessageQueue。


/**
 * 消息拉取模式
 **/
public class PullConsumer {
    private static final Logger log = LoggerFactory.getLogger("PullConsumer");
    //保存上一次消費的消息位置
    private static final Map offsetTable = new HashMap();

    public static void main(String[] args) throws MQClientException {
        //實例化pullConsumer
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("rocketmq-cluster");
        consumer.setNamesrvAddr(MQConstant.Master_Slave);
        consumer.start();
        //獲取topic下所有的隊列
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("test");
        //遍歷消息隊列
        for (MessageQueue mq : mqs) {
            log.info("消息隊列信息: " + mq);
            SINGLE_MQ:
            while (true) {
                try {
                    //設置上次消費消息下標
                    PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                    // 保存消息下次讀取的 offset
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
                    switch (pullResult.getPullStatus()) {
                        //根據結果狀態,如果找到消息,批量消費消息
                        case FOUND:
                            List<MessageExt> messageExtList = pullResult.getMsgFoundList();
                            for (MessageExt m : messageExtList) {
                                log.info("topic:{}; getQueueId:{}; offset:{}; 消息內容:{}",m.getTopic(),m.getQueueId(),m.getQueueOffset(),new String(m.getBody()));
                            }
                            break;
                        case NO_MATCHED_MSG:
                            log.warn("沒有匹配的信息");
                            break;
                        case NO_NEW_MSG:
                            log.warn("沒有新的的信息");
                            break SINGLE_MQ;
                        case OFFSET_ILLEGAL:
                            log.warn("OFFSET_ILLEGAL");
                            break;
                        default:
                            break;
                    }
                } catch (Exception e) {
                    log.error("消息消費出現異常:");
                    e.printStackTrace();
                }
            }
        }
        consumer.shutdown();
    }
    //保存上次消費的消息下標,這裏使用了一個全局HashMap來保存
    private static void putMessageQueueOffset(MessageQueue mq, long offset) {
        offsetTable.put(mq, offset);
    }

    //獲取上次消費的消息的下表 這裏可以保存在硬盤或redis中 ConsumerName-topic-queueId做key
    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = (Long) offsetTable.get(mq);
        if (offset != null) {
            return offset;
        }
        return 0;
    }
}

4、消息落地方式

4.1 同步刷盤和異步刷盤
RocketMQ的消息是存儲到磁盤上的,這樣既能保證斷電後恢復,又可以讓存儲的消息量超出內存的限制。
消息在通過Producer寫入RocketMQ的時候,有兩種寫磁盤方式:
異步刷盤方式: 在返回寫成功狀態時,消息可能只是被寫入了內存的PAGECACHE,寫操作的返回快,
吞吐量大;當內存裏的消息量積累到一定程度時,統一觸發寫磁盤操作,快速寫入
同步刷盤方式: 在返回寫成功狀態時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE後,立刻通知刷盤線程刷盤,然後等待刷盤完成,刷盤線程執行完成後喚醒等待的線程,返回消息寫成功的狀態。
4.12同步雙寫和異步複製
異步複製和同步雙寫主要是主和從的關係。消息需要實時消費的,就需要採用主從模式部署
異步複製: 比如這裏有一主一從,我們發送一條消息到主節點之後,這樣消息就算從producer端發送成功了,然後通過異步複製的方法將數據複製到從節點
同步雙寫: 比如這裏有一主一從,我們發送一條消息到主節點之後,這樣消息就並不算從producer端發送成功了,需要通過同步雙寫的方法將數據同步到從節點後, 纔算數據發送成功。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章