RocketMQ 入門

沒事幹寫部落格玩發表於2020-12-10

引言

發現寫過了RabbitMQKafka,這次補上RocketMQ,之後有空會講講這幾個訊息中介軟體的實際場景選型和選擇原因。

本篇文章不寫整合Springboot,因為Springboot你可能看不到這些細節

引入依賴

<dependency>
	<groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>自己選個版本</version>
</dependency>

NameServer

這裡簡單說一下在RocketMQ中的一些概念,RocketMQbroker關係是靠NameServer來維護的,這和Kafka依賴於Zookeeper的道理是一樣的,都是借用另外一箇中介軟體去實現服務發現(broker發現),而且RocketMQ經常會和Kafka做對比。

Topic 和 Tag

  • Topic

    訊息主題,通過Topic 對不同的業務訊息進行分類。

  • Tag

    訊息標籤,用來進一步區分某個 Topic下的訊息分類 ,訊息從生產者發出即帶上的屬性。

阿里的適用場景:

  • 訊息型別是否一致:如普通訊息、事務訊息、定時(延時)訊息、順序訊息,不同的訊息型別使用不同的 Topic,無法通過 Tag 進行區分。
  • 業務是否相關聯:沒有直接關聯的訊息,如淘寶交易訊息,京東物流訊息使用不同的 Topic 進行區分;而同樣是天貓交易訊息,電器類訂單、女裝類訂單、化妝品類訂單的訊息可以用 Tag 進行區分。

Producer 傳送訊息

同步訊息

訊息的Producer作為訊息生產者一直會等待到MQ返回資訊,一般很重要的訊息會用到同步訊息。

DefaultMQProducer producer = new DefaultMQProducer("test_group");

producer.setNamesrvAddr("192.168.0.102:9876;192.168.0.111:9876");

producer.start();

Message msg = new Message("test_topic","sync_tag","message body".getBytes());

SendResult result = producer.send(msg);

//在result中我們可以獲得傳送狀態,訊息ID,佇列ID
SendStatus status = result.getSendStatus();
String msgId = result.GetMsgId(); //還可以獲取offsetMsgId msgId並非一定不重複
int queueId = result.getMessageQueue().getQueueId();

System.out.println("status:" + status + " msgId:" + msgId + " queueId:" + queueId);

producer.shutdown();

非同步訊息

非同步訊息同步訊息的區別就不說了,API的區別在於send的第2個引數需要傳遞一個SendCallback(需要注意的是send方法過載了很多個第2個引數也可以不是回撥,下面會寫順序訊息)。

DefaultMQProducer producer = new DefaultMQProducer("test_group");

producer.setNamesrvAddr("192.168.0.102:9876;192.168.0.111:9876");

producer.start();

Message msg = new Message("test_topic","async_tag","async message body".getBytes());

CountDownLatch latch = new CountDownLatch(1);

producer.send(msg, new SendCallback(){
    //成功
    public void onSuccess(SendResult sendResult){
        System.out.println("成功結果:" + sendResult);
        latch.countDown();
    }
    //失敗
    public void onException(Throwable e){
        System.out.println("傳送錯誤" + e);
        latch.countDown();
    }
});

//避免主執行緒提前結束
latch.await();

producer.shutdown();

單向訊息

同步訊息非同步訊息都可以算是雙向訊息,因為無論成功還是失敗,都會有返回結果單向訊息的意思就是只管發不管結果,無論傳送成功還是失敗,都不關心(不會有返回結果),一般傳送日誌會用到。

DefaultMQProducer producer = new DefaultMQProducer("test_group");

producer.setNamesrvAddr("192.168.0.102:9876;192.168.0.111:9876");

producer.start();

Message msg = new Message("test_topic","oneway_tag","oneway message body".getBytes());

//無返回值
producer.sendOneway(msg);

producer.shutdown();

Consumer 消費訊息

PushConsumer及廣播模式

我們先看看broker主動推訊息的方式如何消費訊息

注意:在RabbitMQ中想要不輪詢消費必須不同佇列,在Kafka中必須group名不同,但是RocketMQ中就算同group同topic也可以設定(setMessageModel)廣播模式

//broker主動push的方式
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_group");

consumer.setNamesrvAddr("192.168.0.102:9876;192.168.0.111:9876");

//topic和tag 訂閱 ,tag支援過濾,具體可以看看文件
//consumer.subscribe("test_topic","sync_tag");
//consumer.subscribe("test_topic","sync_tag || async_tag");
consumer.subscribe("test_topic","*");

//設定為廣播模式,如果不設定多個消費者 group相同且topic也相同會是輪詢模式。
consumer.setMessageModel(MessageModel.BROADCASTING);

//註冊監聽器
consumer.registerMessageListener(new MessageListenerConcurrently(){
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context){
        System.out.println(msgs);
        //返回消費成功狀態
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

consumer.start();

進階

順序訊息

順序訊息就是字面意思,按照傳送順序來消費(FIFO先入先出)。

應用場景例如:狀態機 建立 付款 成功 失敗。相同訂單號進入同一個佇列,那麼消費時同一個佇列的訊息就不會出現亂序問題。

注意:Message ID 有可能出現衝突(重複)的情況,所以真正需要冪等驗證可以使用message.setKey("ORDERID");消費訊息時可以使用 message.getKey()來做冪等處理

順序Producer

DefaultMQProducer producer = new DefaultMQProducer("test_group");

producer.setNamesrvAddr("192.168.0.102:9876;192.168.0.111:9876");

producer.start();

//模擬3個訊息 1001為key(訂單號)
Message msgBuilding = new Message("order_topic", "order_tag", "1001" , "building...".getBytes());
Message msgPurchasing = new Message("order_topic", "order_tag", "1001", "Purchasing".getBytes());
Message msgSuccessfully = new Message("order_topic", "order_tag", "1001", "Successfully".getBytes());

Message msgBuilding2 = new Message("order_topic", "order_tag", "1002" , "building...".getBytes());
Message msgPurchasing2 = new Message("order_topic", "order_tag", "1002", "Purchasing".getBytes());
Message msgSuccessfully2 = new Message("order_topic", "order_tag", "1002", "Successfully".getBytes());

//建立一個佇列選擇器
MessageQueueSelector queueSelector = new MessageQueueSelector(){
    /**
    * 
    * @param mqs: 佇列集合
    * @param msg: 訊息物件
    * @param arg: 額外引數 producer.send如果第二個引數是佇列選擇器,那麼arg就是第三個引數
    * @return: 佇列
    *
    */
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, object arg){
        Long orderId = (Long) arg;//這裡也可以使用msg.getKey();
        int index = (int)(orderId % mqs.size()); //同餘演算法即可,相同訂單絕對會進入同一個佇列
        return mqs.get(index); //返回選擇到的佇列
    }
  
};

//下面3條訊息經過佇列選擇器會進入相同的佇列,因為他們的key(訂單號)是相同的
SendResult result = producer.send(msgBuilding, queueSelector, Long.parseLong(msgBuilding.getKey()));
System.out.println("msgBuilding res:" + result);

result = producer.send(msgPurchasing, queueSelector, Long.parseLong(msgBuilding.getKey()));
System.out.println("msgPurchasing res:" + result);

result = producer.send(msgSuccessfully, queueSelector, Long.parseLong(msgBuilding.getKey()));
System.out.println("msgSuccessfully res:" + result);

result = producer.send(msgBuilding2, queueSelector, Long.parseLong(msgBuilding.getKey()));
System.out.println("msgBuilding res:" + result);

result = producer.send(msgPurchasing2, queueSelector, Long.parseLong(msgBuilding.getKey()));
System.out.println("msgPurchasing res:" + result);

result = producer.send(msgSuccessfully2, queueSelector, Long.parseLong(msgBuilding.getKey()));
System.out.println("msgSuccessfully res:" + result);

producer.shutdown();

順序Consumer


DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test_group");

consumer.setNamesrvAddr("192.168.0.102:9876;192.168.0.111:9876");

consumer.subscribe("order_topic","*");

//注意訊息監聽器不是Concurrently了
consumer.registerMessageListener(new MessageListenerOrderly(){
    //返回消費成功狀態 這裡也不是ConcurrentlyStatus了
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context){
        for (MessageExt msg : msgs){
            System.out.println(msg);
        }
        
        return ConsumeOrderlyStatus.SUCCESS;
    }
});

consumer.start();

延遲訊息

不解釋了啊,之前在RabbitMQ文章中已經寫了延遲訊息的意思。

需要注意的是:RocketMQ延遲時間不能自定義,它只支援固定的一個級別(列表)延遲時間必須是這個級別中的一項(從1級開始)private string messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";,傳送者對訊息設定delayTimeLevel,消費者沒有什麼要求。

DefaultMQProducer producer = new DefaultMQProducer("test_group");

producer.setNamesrvAddr("192.168.0.102:9876;192.168.0.111:9876");

producer.start();

Message msg = new Message("delay_topic","test_delay","message body".getBytes());
msg.setDelayTimeLevel(3);//設定訊息的延遲時間級別

SendResult result = producer.send(msg);

System.out.println("result:" + result);

producer.shutdown();

TransactionMQProducer 傳送事務訊息

TransactionMQProducer producer = new TransactionMQProducer("test_group");

producer.setNamesrvAddr("192.168.0.102:9876;192.168.0.111:9876");

producer.setTransactionListener(new TransactionListener(){
    //執行本地事務方法
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg){
        if ("tag1".equals(msg.getTags())){  // 如果訊息標籤為tag1就提交
            return LocalTransactionState.COMMIT_MESSAGE;
        }else if ("tag2".equals(msg.getTags())){  // 如果訊息標籤為tag2就回滾
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        
        return LocalTransactionState.UNKNOW; //否則返回未知,返回未知後會進入回查方法
    }
    
    //回查方法
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg){
        System.out.println("checkLocalTransaction tag:" + msg.getTags());
        return LocalTransactionState.COMMIT_MESSAGE; //回查後我們可以選擇回滾或提交
    }
});

producer.start();

Message msg = new Message("transaction_topic","tag1","message body".getBytes());
Message msg2 = new Message("transaction_topic","tag2","message body".getBytes());
Message msg3 = new Message("transaction_topic","tag3","message body".getBytes());
// 第二個引數 在TransactionListener的executeLocalTransaction方法裡arg會被傳進去
SendResult result = producer.sendMessageInTransaction(msg, null);
System.out.println("result:" + result);

result = producer.sendMessageInTransaction(msg2, null);
System.out.println("result2:" + result);

result = producer.sendMessageInTransaction(msg3, null);
System.out.println("result3:" + result);

producer.shutdown();

相關文章