消息隊列——RocketMQ示例

消息隊列——RocketMQ示例

1. 簡介

  • 定位:分佈式消息中間件、消息隊列
  • 語言:Java
  • 性能:10萬級吞吐量,ms級時效性
  • 可靠性:分佈式架構,可靠性非常高
  • 其他:由阿里在2016年貢獻至Apache基金會,已成爲頂級項目。歷經雙十一考驗,能夠處理萬億級別的消息。

2. 集羣架構與工作流程

  • 集羣架構示意圖(來自互聯網)
    集羣架構示意圖
  • 工作流程
    • 首先,啓動NameServer集羣,負責管理Broker、Producer、Consumer的連接、負責管理Topic的元信息。(作用類似於Kafka中的ZooKeeper)
    • 接着,啓動Broker集羣,註冊到NameServer集羣中,保持心跳。一個集羣可以由多個Broker組組成,一個Broker組(由BrokerName標識)包括Master、Slave(由BrokerId標識),Master負責接收生產的數據,Slave負責備份Master的數據。(同Kafka差異較大)
      • 同步:數據生產到Master,並將數據同步到Slave後,才向生產端迴應Ack
      • 異步:數據生產到Master後,馬上向生產端迴應Ack,不管Slave數據是否已經同步
    • 創建一個Topic,Topic可以手動指定將數據存儲到哪些Broker,也可以自動分配。(沒有Topic的話,發送時也能自動創建)
    • Producer生產數據時,會與NameServer集羣保持長連接,定期獲取對應Topic的信息,找到對應Master節點併發送數據。注意,Producer只能向Broker中的Master生產數據。
    • Consumer消費數據時,會與NameServer集羣保持長連接,定期獲取對應Topic的信息,找到對應Master、Slave節點並獲取數據(包括Push、Pull)。注意,Consumer可以從Master或Slave消費數據(由Broker配置決定)。
      • Push方式: 由Broker推消息到Consumer
      • Pull方式: 由Consumer主動從Broker拉數據
    • 消息順序:一個Broker內有多個MessageQueue,消息在一個MessagQueue內是有序的。(和Kafka類同,Kafka的消息在一個Partition內有序)

3. 簡單示例

  • Maven依賴導包
    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-client</artifactId>
        <version>4.6.0</version>
    </dependency>
    
  • 生產者 (同步、異步、單向)
    import org.apache.rocketmq.client.producer.DefaultMQProducer;
    import org.apache.rocketmq.client.producer.SendCallback;
    import org.apache.rocketmq.client.producer.SendResult;
    import org.apache.rocketmq.common.message.Message;
    
    /**
     * Description: RocketMQ 簡單的Producer示例 (同步、異步、單向)
     * <br/>
     * Date: 2020/1/6 17:28
     *
     * @author ALion
     */
    public class SimpleProducer {
    
        public static void main(String[] args) throws Exception {
            // 創建Producer
            // 實例化Producer對象,並指定生產者組名爲producer_name
            DefaultMQProducer producer = new DefaultMQProducer("producer_name");
            // 指定NameServer集羣地址,多個用;分隔
            producer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876");
            // 啓動Producer
            producer.start();
    
            // 開始生產數據
            for (int i = 0; i < 100; i++) {
                // 構建消息對象,包括Topic、Tag、MessageBody
                // Topic: 主題
                // Tag: 一個主題下面可以分多個Tag。可以理解爲一個業務功能(Topic)下有多種消息,Tag用於對消息分類。
                // MessageBody: 你要發送的消息,因爲網絡傳輸需要字節碼,所以要轉換一下
                Message msg = new Message(
                        "Topic_Test",
                        "Tag_A",
                        ("Hello RocketMQ " + i).getBytes()
                );
    
                // 【同步發送方式】
                // 發送消息到RocketMQ集羣
                SendResult sendResult = producer.send(msg);
                // 打印結果
                System.out.printf("%s%n", sendResult);
    
                // 【異步發送方式】
    //            producer.send(msg, new SendCallback() {
    //                @Override
    //                public void onSuccess(SendResult sendResult) {
    //                    // 發送成功時調用
    //                    System.out.printf("%s%n", sendResult);
    //                }
    //
    //                @Override
    //                public void onException(Throwable throwable) {
    //                    // 發送失敗時調用
    //                    throwable.printStackTrace();
    //                }
    //            });
    
                // 【單向發送方式】(也是異步,但是不獲取響應)
    //            producer.sendOneway(msg);
            }
    
            // 關閉Producer
            producer.shutdown();
        }
    
    }
    
  • 消費者 (Push方式、Pull方式)
    import org.apache.rocketmq.client.consumer.DefaultLitePullConsumer;
    import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
    import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
    import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
    import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
    import org.apache.rocketmq.common.message.MessageExt;
    import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
    
    import java.util.List;
    
    /**
     * Description: RocketMQ 簡單的Consumer示例 (Push方式、Pull方式)
     * <br/>
     * Date: 2020/1/6 17:58
     *
     * @author ALion
     */
    public class SimpleConsumer {
    
        public static void main(String[] args) throws Exception {
            // 創建Consumer
            // 實例化Consumer對象,並指定消費者組名爲push_consumer_name
            // 【Push方式】 由Broker推消息到Consumer
            DefaultMQPushConsumer pushConsumer = new DefaultMQPushConsumer("push_consumer_name");
            // 指定NameServer集羣地址,多個用;分隔
            pushConsumer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876");
            // 設置訂閱的Topic與Tag
            // 訂閱多個Tag,用||分隔,例如 "Tag_A || Tag_B"、"*"
            pushConsumer.subscribe("Topic_Test", "Tag_A");
    
            // 設定消費模式 CLUSTERING與BROADCASTING
            // CLUSTERING: 負載均衡(默認)。多個消費者消費時,分別消費所有消息的一部分。
            // BROADCASTING: 廣播模式。多個消費者消費時,都全部消費。
    //        pushConsumer.setMessageModel(MessageModel.CLUSTERING);
    
            // 註冊一個回調監聽,用於接收消息
            pushConsumer.registerMessageListener(new MessageListenerConcurrently() {
                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                    for (MessageExt messageExt : list) {
                        // 接到的消息是字節碼,需要解碼
                        byte[] body = messageExt.getBody();
                        System.out.println(new String(body));
                    }
    
                    // 回覆 Broker "消費成功"
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
    
            // 啓動Consumer
            pushConsumer.start();
    
            // 【Pull方式】 由Consumer主動從Broker拉數據
    //        DefaultLitePullConsumer pullConsumer = new DefaultLitePullConsumer("pull_consumer_name");
    //        pullConsumer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876");
    //        pullConsumer.subscribe("Topic_Test", "Tag_A");
    //        while (true) {
    //            List<MessageExt> messageExts = pullConsumer.poll(1000); // 超時時間1000ms
    //            for (MessageExt messageExt : messageExts) {
    //                byte[] body = messageExt.getBody();
    //                System.out.println(new String(body));
    //            }
    //        }
        }
    
    }
    

4. 有序消息示例

  • 原理:將需要保證順序的消息發送到同一MessageQueue,即可保證有序
  • 生產者
    import org.apache.rocketmq.client.producer.DefaultMQProducer;
    import org.apache.rocketmq.client.producer.MessageQueueSelector;
    import org.apache.rocketmq.client.producer.SendResult;
    import org.apache.rocketmq.common.message.Message;
    import org.apache.rocketmq.common.message.MessageQueue;
    
    import java.util.List;
    
    /**
     * Description: RocketMQ 有序Producer示例
     * <br/>
     * Date: 2020/1/6 17:28
     *
     * @author ALion
     */
    public class OrderedProducer {
    
        public static void main(String[] args) throws Exception {
            // 創建Producer
            DefaultMQProducer producer = new DefaultMQProducer("producer_name");
            producer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876");
            producer.start();
    
            // 開始生產數據
            for (int i = 0; i < 100; i++) {
                // orderId 消息的唯一標識
                // 用實際生產環境的用戶id等替代該值,能夠保證該用戶數據的有序性
                int orderId  = i % 10;
                Message msg = new Message(
                        "Topic_Ordered",
                        "Tag_A",
                        "KEY" + i, // 標識唯一一條消息
                        ("Hello RocketMQ " + i).getBytes()
                );
    
                // 發送消息到RocketMQ集羣
                SendResult sendResult = producer.send(
                        msg,
                        // MessageQueueSelector用於決定消息發送到哪個隊列
                        new MessageQueueSelector() {
                            @Override
                            public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                                // arg 即 orderId
                                // 同一個orderId的數據應該發送到同一個MessageQueue
                                int idx = (Integer) arg % mqs.size();
                                return mqs.get(idx);
                            }
                        },
                        orderId
                );
                // 打印結果
                System.out.printf("%s%n", sendResult);
            }
    
            // 關閉Producer
            producer.shutdown();
        }
    
    }
    
  • 消費者
    import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
    import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
    import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
    import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
    import org.apache.rocketmq.client.exception.MQClientException;
    import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
    import org.apache.rocketmq.common.message.MessageExt;
    
    import java.util.List;
    
    /**
     * Description: RocketMQ 有序Consumer示例
     * <br/>
     * Date: 2020/1/6 19:35
     *
     * @author ALion
     */
    public class OrderedConsumer {
    
        public static void main(String[] args) throws MQClientException {
            // 創建Consumer
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");
            // 設置從最開始的位置消費,默認是最後的位置開始(CONSUME_FROM_LAST_OFFSET)
            consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
            // 訂閱並監聽消息
            consumer.subscribe("Topic_Ordered", "Tag_A");
            // 注意,這裏需要傳 MessageListenerOrderly
            consumer.registerMessageListener(new MessageListenerOrderly() {
                @Override
                public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                    for (MessageExt messageExt : msgs) {
                        System.out.println(
                                Thread.currentThread().getName() +
                                " Receive New Messages: " + new String(messageExt.getBody())
                        );
                    }
    
                    return ConsumeOrderlyStatus.SUCCESS;
                }
            });
    
            // 啓動Consumer
            consumer.start();
        }
    
    }
    

5. 事務消息示例

  • 事務機制示意圖(來自互聯網)
    事務機制示意圖
    1. 生產者生產消息到RocketMQ集羣
    2. RocketMQ集羣返回Ack,表示生產成功
    3. 生產者處理本地事務
    4. 事務的狀態
      • 如果成功,提交 LocalTransactionState.COMMIT_MESSAGE
      • 如果失敗,回滾 LocalTransactionState.ROLLBACK_MESSAGE
      • 仍在處理中, 未知 LocalTransactionState.UNKNOW
    5. RocketMQ集羣在未收到COMMIT_MESSAGE或ROLLBACK_MESSAGE前,會定時回查生產者
    6. 確認消息事務狀態(提交、回滾、未知),直到狀態變爲提交或回滾
  • 生產者
    import org.apache.rocketmq.client.producer.SendResult;
    import org.apache.rocketmq.client.producer.TransactionMQProducer;
    import org.apache.rocketmq.common.message.Message;
    
    /**
     * Description: RocketMQ 事務Producer示例
     * <br/>
     * Date: 2020/1/6 19:51
     *
     * @author ALion
     */
    public class TransactionProducer {
    
        public static void main(String[] args) throws Exception {
            // 創建Producer
            TransactionMQProducer producer = new TransactionMQProducer("transaction_producer_name");
            producer.setNamesrvAddr("192.168.1.110:9876;192.168.1.111:9876;192.168.1.112:9876");
            // 設置事務監聽器,用於RocketMQ集羣來回查事務處理狀態
            producer.setTransactionListener(new MyTransactionListener());
            producer.start();
    
            // 開始生產數據
            for (int i = 0; i < 100; i++) {
                // i 能除盡5時,給一個亂碼,用於模擬事務失敗回滾
                String suffix = i % 5 == 0 ? "!@#" : i + "";
                String body = "Hello RocketMQ ," + suffix;
                Message msg = new Message(
                        "Topic_Transaction",
                        "Tag_A",
                        body.getBytes()
                );
    
                // 發送消息到RocketMQ集羣
                // null表示對所有消息進行事務控制
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                // 打印結果
                System.out.printf("%s%n", sendResult);
    
            }
    
            // 關閉Producer
            producer.shutdown();
        }
    
    }
    
  • 事務處理監聽器
    import org.apache.rocketmq.client.producer.LocalTransactionState;
    import org.apache.rocketmq.client.producer.TransactionListener;
    import org.apache.rocketmq.common.message.Message;
    import org.apache.rocketmq.common.message.MessageExt;
    
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * Description: 事務處理監聽器
     * <br/>
     * Date: 2020/1/6 20:16
     *
     * @author ALion
     */
    public class MyTransactionListener implements TransactionListener {
    
        // 設置一個Map,用於標識每條消息的事務狀態
        // value=0 表示處理失敗
        // value=1 表示處理成功
        // value=其他值 表示未知
        private ConcurrentHashMap<String, Integer> transactionMap = new ConcurrentHashMap<>();
    
        @Override
        public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
            // 處理事務
    
            // 一開始數據還在處理,先設置一個其他值(例如3),表示未知
            transactionMap.put(msg.getTransactionId(), 3);
    
            // 可以直接阻塞式處理
            // 也可以開子線程(或線程池)進行異步處理
            // 處理完後需要更新transactionMap中的狀態
            new Thread(() -> doSomething(msg, arg)).start();
    
            // 還在處理中,先返回未知狀態
            return  LocalTransactionState.UNKNOW;
        }
    
        private void doSomething(Message msg, Object arg) {
            String content = new String(msg.getBody());
            String[] fields = content.split(",");
            try {
                String txt = fields[0];
                int number = Integer.parseInt(fields[1]);
                System.out.println("txt = " + txt + ", number = " + number);
    
                // 解析成功,修改狀態爲1,事務提交
                transactionMap.put(msg.getTransactionId(), 1);
            } catch (NumberFormatException e) {
                e.printStackTrace();
                // 解析失敗,修改狀態爲0,事務回滾
                transactionMap.put(msg.getTransactionId(), 0);
            }
        }
    
        @Override
        public LocalTransactionState checkLocalTransaction(MessageExt msg) {
            // 如果executeLocalTransaction返回 LocalTransactionState.UNKNOW
            // MQ將會定時回查該方法,以確定事務狀態
            Integer integer = transactionMap.get(msg.getTransactionId());
            if (integer != null) {
                switch (integer) {
                    case 0:
                        return LocalTransactionState.COMMIT_MESSAGE;
                    case 1:
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    default:
                        return LocalTransactionState.UNKNOW;
                }
            }
    
            return LocalTransactionState.UNKNOW;
        }
    
    }
    
發佈了143 篇原創文章 · 獲贊 52 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章