4-RocketMQ基礎

LZC發表於2021-07-07

1 技術架構

RocketMQ架構上主要分為四部分,如上圖所示:

  • Producer:訊息釋出的角色,支援分散式叢集方式部署。Producer透過MQ的負載均衡模組選擇相應的Broker叢集佇列進行訊息投遞,投遞的過程支援快速失敗並且低延遲。
  • Consumer:訊息消費的角色,支援分散式叢集方式部署。支援以push推,pull拉兩種模式對訊息進行消費。同時也支援叢集方式和廣播方式的消費,它提供實時訊息訂閱機制,可以滿足大多數使用者的需求。
  • NameServer:NameServer是一個非常簡單的Topic路由註冊中心,其角色類似Dubbo中的zookeeper,支援Broker的動態註冊與發現。主要包括兩個功能:Broker管理,NameServer接受Broker叢集的註冊資訊並且儲存下來作為路由資訊的基本資料。然後提供心跳檢測機制,檢查Broker是否還存活;路由資訊管理,每個NameServer將儲存關於Broker叢集的整個路由資訊和用於客戶端查詢的佇列資訊。然後Producer和Conumser透過NameServer就可以知道整個Broker叢集的路由資訊,從而進行訊息的投遞和消費。NameServer通常也是叢集的方式部署,各例項間相互不進行資訊通訊。Broker是向每一臺NameServer註冊自己的路由資訊,所以每一個NameServer例項上面都儲存一份完整的路由資訊。當某個NameServer因某種原因下線了,Broker仍然可以向其它NameServer同步其路由資訊,Producer,Consumer仍然可以動態感知Broker的路由的資訊。
  • BrokerServer:Broker主要負責訊息的儲存、投遞和查詢以及服務高可用保證,為了實現這些功能,Broker包含了以下幾個重要子模組。
  1. Remoting Module:整個Broker的實體,負責處理來自clients端的請求。
  2. Client Manager:負責管理客戶端(Producer/Consumer)和維護Consumer的Topic訂閱資訊
  3. Store Service:提供方便簡單的API介面處理訊息儲存到物理硬碟和查詢功能。
  4. HA Service:高可用服務,提供Master Broker 和 Slave Broker之間的資料同步功能。
  5. Index Service:根據特定的Message key對投遞到Broker的訊息進行索引服務,以提供訊息的快速查詢。

2 部署架構

RocketMQ 網路部署特點

  • NameServer是一個幾乎無狀態節點,可叢集部署,節點之間無任何資訊同步。
  • Broker部署相對複雜,Broker分為Master與Slave,一個Master可以對應多個Slave,但是一個Slave只能對應一個Master,Master與Slave 的對應關係透過指定相同的BrokerName,不同的BrokerId 來定義,BrokerId為0表示Master,非0表示Slave。Master也可以部署多個。每個Broker與NameServer叢集中的所有節點建立長連線,定時註冊Topic資訊到所有NameServer。 注意:當前RocketMQ版本在部署架構上支援一Master多Slave,但只有BrokerId=1的從伺服器才會參與訊息的讀負載。
  • Producer與NameServer叢集中的其中一個節點(隨機選擇)建立長連線,定期從NameServer獲取Topic路由資訊,並向提供Topic 服務的Master建立長連線,且定時向Master傳送心跳。Producer完全無狀態,可叢集部署。
  • Consumer與NameServer叢集中的其中一個節點(隨機選擇)建立長連線,定期從NameServer獲取Topic路由資訊,並向提供Topic服務的Master、Slave建立長連線,且定時向Master、Slave傳送心跳。Consumer既可以從Master訂閱訊息,也可以從Slave訂閱訊息,消費者在向Master拉取訊息時,Master伺服器會根據拉取偏移量與最大偏移量的距離(判斷是否讀老訊息,產生讀I/O),以及從伺服器是否可讀等因素建議下一次是從Master還是Slave拉取。

結合部署架構圖,描述叢集工作流程:

  • 啟動NameServer,NameServer起來後監聽埠,等待Broker、Producer、Consumer連上來,相當於一個路由控制中心。
  • Broker啟動,跟所有的NameServer保持長連線,定時傳送心跳包。心跳包中包含當前Broker資訊(IP+埠等)以及儲存所有Topic資訊。註冊成功後,NameServer叢集中就有Topic跟Broker的對映關係。
  • 收發訊息前,先建立Topic,建立Topic時需要指定該Topic要儲存在哪些Broker上,也可以在傳送訊息時自動建立Topic。
  • Producer傳送訊息,啟動時先跟NameServer叢集中的其中一臺建立長連線,並從NameServer中獲取當前傳送的Topic存在哪些Broker上,輪詢從佇列列表中選擇一個佇列,然後與佇列所在的Broker建立長連線從而向Broker發訊息。
  • Consumer跟Producer類似,跟其中一臺NameServer建立長連線,獲取當前訂閱Topic存在哪些Broker上,然後直接跟Broker建立連線通道,開始消費訊息。

Windows安裝

  1. 下載
https://mirror.bit.edu.cn/apache/rocketmq/4.7.0/rocketmq-all-4.7.0-bin-release.zip
  1. 解壓

  2. 配置RocketMQ環境變數,ROCKETMQ_HOME=rocketmq解壓目錄

  3. 啟動Name Server。在ROCKETMQ_HOME/bin目錄下雙擊執行mqnamesrv.cmd,出現如下資訊表示啟動成功,保持命令視窗開啟(若視窗一閃而過,說明沒有配置環境變數,請先配置環境變數)

Java HotSpot(TM) 64-Bit Server VM warning: Using the DefNew young collector with the CMS collector is deprecated and will likely be removed in a future release
Java HotSpot(TM) 64-Bit Server VM warning: UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release.
The Name Server boot success. serializeType=JSON
  1. 啟動broker。開啟另一個windows終端cmd,進入ROCKETMQ_HOME/bin目錄,

    輸入mqbroker -n 127.0.0.1:9876啟動broker,保持mqbroker執行,不要關閉這個終端。

D:\software\rocketmq-all-4.7.0-bin-release\bin>mqbroker -n 127.0.0.1:9876
The broker[PQSZ-L0039, 10.178.42.122:10911] boot success. serializeType=JSON and name server is 127.0.0.1:9876

Linux安裝

下載

wget https://mirror.bit.edu.cn/apache/rocketmq/4.7.0/rocketmq-all-4.7.0-bin-release.zip

解壓

這裡解壓到當前目錄的soft資料夾下面。

unzip rocketmq-all-4.7.0-bin-release.zip -d soft/

啟動/關閉Name Server

啟動

[root@localhost rocketmq-all-4.7.0-bin-release]# nohup sh bin/mqnamesrv &

驗證是否啟動OK:

tail -f ~/logs/rocketmqlogs/namesrv.log

# 如果成功啟動,能看到類似如下的日誌:
2020-04-25 09:31:31 INFO main - The Name Server boot success. serializeType=JSON

執行失敗

RocketMQ預設的虛擬機器記憶體較大,啟動Broker如果因為記憶體不足失敗,需要編輯如下兩個配置檔案,修改JVM記憶體大小

runserver.sh(Windows對應著runserver.cmd)

JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn512m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"

runbroker.sh(Windows對應著runbroker.cmd)

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

關閉

bin/mqshutdown namesrv
# 輸出如下資訊說明停止成功
The mqnamesrv(2134) is running...
Send shutdown request to mqnamesrv(2134) OK

啟動/關閉Broker

啟動

nohup sh bin/mqbroker -n localhost:9876 &

驗證是否啟動OK:

tail -f ~/logs/rocketmqlogs/broker.log

# 如果啟動成功,能看到類似如下的日誌:
2020-04-25 09:54:24 INFO main - The broker[localhost.localdomain, 192.168.192.140:10911] boot success. serializeType=JSON and name server is localhost:9876

關閉

sh bin/mqshutdown broker

# 輸出如下資訊說明停止成功
The mqbroker(2604) is running...
Send shutdown request to mqbroker(2604) OK

訊息控制檯

下載控制檯程式碼

git clone https://github.com/apache/rocketmq-externals.git

下載程式碼後,開啟rocketmq-console,這是一個SpringBoot工程,進入配置檔案配置Name Server的地址

rocketmq.config.namesrvAddr=192.168.192.140:9876

啟動SpringBoot專案,瀏覽器輸入localhost:8080

依賴

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

生產同步訊息

這種可靠性同步地傳送方式使用的比較廣泛,比如:重要的訊息通知,簡訊通知。

public class SyncProducer {
    public static void main(String[] args) throws Exception {
        // 例項化訊息生產者Producer
        DefaultMQProducer producer = new DefaultMQProducer("GroupNameDemo");
        // 設定NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 啟動Producer例項
        producer.start();
        for (int i = 0; i < 10; i++) {
            // 建立訊息,並指定Topic,Tag和訊息體
            Message msg = new Message("TopicTest" /* Topic */,
                    "TagA" /* Tag*/,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
            );
            // 傳送訊息到一個Broker
            SendResult sendResult = producer.send(msg);
            // 透過sendResult返回訊息是否成功送達
            System.out.printf("%s%n", sendResult);
        }
        // 如果不再傳送訊息,關閉Producer例項。
        producer.shutdown();
    }
}

傳送非同步訊息

非同步訊息通常用在對響應時間敏感的業務場景,即傳送端不能容忍長時間地等待Broker的響應。

public class AsyncProducer {
    public static void main(String[] args) throws Exception {
        // 例項化訊息生產者Producer
        DefaultMQProducer producer = new DefaultMQProducer("GroupNameDemo");
        // 設定NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 啟動Producer例項
        producer.start();
        producer.setRetryTimesWhenSendAsyncFailed(0);

        int messageCount = 10;
        // 根據訊息數量例項化倒數計時計算器
        final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount);
        for (int i = 0; i < messageCount; i++) {
            final int index = i;
            // 建立訊息,並指定Topic,Tags、keys、訊息體
            Message msg = new Message("TopicTest",
                    "TagA",
                    "OrderID188",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
            // SendCallback接收非同步返回結果的回撥
            producer.send(msg, new SendCallback() {
                @Override
                public void onSuccess(SendResult sendResult) {
                    System.out.printf("%-10d OK %s %n", index,
                            sendResult);
                }
                @Override
                public void onException(Throwable e) {
                    System.out.printf("%-10d Exception %s %n", index, e);
                    e.printStackTrace();
                }
            });
        }
        // 等待5s
        countDownLatch.await(5, TimeUnit.SECONDS);
        // 如果不再傳送訊息,關閉Producer例項。
        producer.shutdown();
    }
}

單向傳送訊息

這種方式主要用在不特別關心傳送結果的場景,例如日誌傳送。

public class OnewayProducer {
    public static void main(String[] args) throws Exception{
        // 例項化訊息生產者Producer
        DefaultMQProducer producer = new DefaultMQProducer("GroupNameDemo");
        // 設定NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 啟動Producer例項
        producer.start();
        for (int i = 0; i < 10; i++) {
            // 建立訊息,並指定Topic,Tag和訊息體
            Message msg = new Message("TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
            );
            // 傳送單向訊息,沒有任何返回結果
            producer.sendOneway(msg);
        }
        // 如果不再傳送訊息,關閉Producer例項。
        producer.shutdown();
    }
}

消費訊息

public class Consumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {

        // 例項化消費者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("GroupNameDemo");

        // 設定NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");

        // 訂閱一個或者多個Topic,以及Tag來過濾需要消費的訊息,多個tag之間用||分隔,* 代表所有
        consumer.subscribe("TopicTest", "*");
        // 註冊回撥實現類來處理從broker拉取回來的訊息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                // 標記該訊息已經被成功消費
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 啟動消費者例項
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

預設情況下,每個Topic有4個佇列,每一個佇列裡面的訊息是能夠保證被順序消費的,所以我們需要保證需要順序消費的訊息能夠被儲存在同一個佇列,因此在傳送訊息時需要制定這條訊息應該被儲存到哪個佇列裡面。

訊息有序指的是可以按照訊息的傳送順序來消費(FIFO)。RocketMQ可以嚴格的保證訊息有序,可以分為分割槽有序或者全域性有序。

順序消費的原理解析,在預設的情況下訊息傳送會採取Round Robin輪詢方式把訊息傳送到不同的queue(分割槽佇列);而消費訊息的時候從多個queue上拉取訊息,這種情況傳送和消費是不能保證順序。但是如果控制傳送的順序訊息只依次傳送到同一個queue中,消費的時候只從這個queue上依次拉取,則就保證了順序。當傳送和消費參與的queue只有一個,則是全域性有序;如果多個queue參與,則為分割槽有序,即相對每個queue,訊息都是有序的。

下面用訂單進行分割槽有序的示例。一個訂單的順序流程是:建立、付款、推送、完成。訂單號相同的訊息會被先後傳送到同一個佇列中,消費時,同一個OrderId獲取到的肯定是同一個佇列。

生產訊息

public class OrderedProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("GroupNameDemo");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
        String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD"};
        // 訂單列表
        List<OrderStep> orderList = new OrderedProducer().buildOrders();

        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = sdf.format(date);
        for (int i = 0; i < 10; i++) {
            // 加個時間字首
            String body = dateStr + " Hello RocketMQ " + orderList.get(i);
            Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes());

            SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    // 這裡的agr引數的值就是orderList.get(i).getOrderId()
                    // 將id相同的訊息放在同一個佇列來保證訊息的順序消費
                    Long id = (Long) arg;  // 根據訂單id選擇傳送queue
                    long index = id % mqs.size();
                    return mqs.get((int) index);
                }
            }, orderList.get(i).getOrderId());//訂單id

            System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
                    sendResult.getSendStatus(),
                    sendResult.getMessageQueue().getQueueId(),
                    body));
        }

        producer.shutdown();
    }

    /**
     * 訂單的步驟
     */
    private static class OrderStep {
        private long orderId;
        private String desc;

        public long getOrderId() {
            return orderId;
        }

        public void setOrderId(long orderId) {
            this.orderId = orderId;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }

        @Override
        public String toString() {
            return "OrderStep{" +
                    "orderId=" + orderId +
                    ", desc='" + desc + '\'' +
                    '}';
        }
    }

    /**
     * 生成模擬訂單資料
     */
    private List<OrderStep> buildOrders() {
        List<OrderStep> orderList = new ArrayList<OrderStep>();

        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(10000L);
        orderDemo.setDesc("建立");
        orderList.add(orderDemo);
        orderDemo = new OrderStep();
        orderDemo.setOrderId(10000L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);
        orderDemo = new OrderStep();
        orderDemo.setOrderId(10000L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);
        orderDemo = new OrderStep();
        orderDemo.setOrderId(10000L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(10001L);
        orderDemo.setDesc("建立");
        orderList.add(orderDemo);
        orderDemo = new OrderStep();
        orderDemo.setOrderId(10001L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);
        orderDemo = new OrderStep();
        orderDemo.setOrderId(10001L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);
        orderDemo = new OrderStep();
        orderDemo.setOrderId(10001L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(10003L);
        orderDemo.setDesc("建立");
        orderList.add(orderDemo);
        orderDemo = new OrderStep();
        orderDemo.setOrderId(10003L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        return orderList;
    }
}

消費訊息

/**
 * 順序訊息消費,帶事務方式(應用可控制Offset什麼時候提交)
 */
public class ConsumerInOrder {

    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("GroupNameDemo");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        /**
         * 設定Consumer第一次啟動是從佇列頭部開始消費還是佇列尾部開始消費<br>
         * 如果非第一次啟動,那麼按照上次消費的位置繼續消費
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        consumer.subscribe("TopicTest", "TagA || TagB || TagC || TagD");
        consumer.registerMessageListener(new MessageListenerOrderly() {
            Random random = new Random();
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 可以看到每個queue有唯一的consume執行緒來消費, 訂單對每個queue(分割槽)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
                }
                try {
                    //模擬業務邏輯處理中...
                    TimeUnit.SECONDS.sleep(random.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        consumer.start();
        System.out.println("Consumer Started.");
    }
}

比如電商裡,提交了一個訂單就可以傳送一個延時訊息,1h後去檢查這個訂單的狀態,如果還是未付款就取消訂單釋放庫存。

啟動消費者

public class ScheduledMessageConsumer {
    public static void main(String[] args) throws Exception {
        // 例項化消費者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer");
        // 設定NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");
        // 訂閱Topics
        consumer.subscribe("TestTopic", "*");
        // 註冊訊息監聽者
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
                for (MessageExt message : messages) {
                    // Print approximate delay time period
                    System.out.println("Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() - message.getStoreTimestamp()) + "ms later");
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 啟動消費者
        consumer.start();
    }
}

## 生產訊息

public class ScheduledMessageProducer {
    public static void main(String[] args) throws Exception {
        // 例項化一個生產者來產生延時訊息
        DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
        // 設定NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 啟動生產者
        producer.start();
        int totalMessagesToSend = 10;
        for (int i = 0; i < totalMessagesToSend; i++) {
            Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
            // 設定延時等級3,這個訊息將在10s之後傳送(現在只支援固定的幾個時間,詳看delayTimeLevel)
            message.setDelayTimeLevel(3);
            // 傳送訊息
            producer.send(message);
        }
        // 關閉生產者
        producer.shutdown();
    }
}

將會看到訊息的消費比儲存時間晚10秒。

延時訊息的使用限制

// org/apache/rocketmq/store/config/MessageStoreConfig.java

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

現在RocketMq並不支援任意時間的延時,需要設定幾個固定的延時等級,從1s到2h分別對應著等級1到18 訊息消費失敗會進入延時訊息佇列,訊息傳送時間與設定的延時等級和重試次數有關,詳見程式碼SendMessageProcessor.java

在大多數情況下,TAG是一個簡單而有用的設計,其可以來選擇您想要的訊息。例如:

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");

消費者將接收包含TAGA或TAGB或TAGC的訊息。但是限制是一個訊息只能有一個標籤,這對於複雜的場景可能不起作用。在這種情況下,可以使用SQL表示式篩選訊息。SQL特性可以透過傳送訊息時的屬性來進行計算。在RocketMQ定義的語法下,可以實現一些簡單的邏輯。下面是一個例子:

------------
| message  |
|----------|  a > 5 AND b = 'abc'
| a = 10   |  --------------------> Gotten
| b = 'abc'|
| c = true |
------------
------------
| message  |
|----------|   a > 5 AND b = 'abc'
| a = 1    |  --------------------> Missed
| b = 'abc'|
| c = true |
------------

基本語法

RocketMQ只定義了一些基本語法來支援這個特性。你也可以很容易地擴充套件它。

  • 數值比較,比如:>,>=,<,<=,BETWEEN,=;
  • 字元比較,比如:=,<>,IN;
  • IS NULL 或者 IS NOT NULL;
  • 邏輯符號 AND,OR,NOT;

常量支援型別為:

  • 數值,比如:123,3.1415;
  • 字元,比如:‘abc’,必須用單引號包裹起來;
  • NULL,特殊的常量
  • 布林值,TRUEFALSE

只有使用push模式的消費者才能用使用SQL92標準的sql語句,介面如下:

public void subscribe(finalString topic, final MessageSelector messageSelector)

啟用配置 (重要 )

使用Filter功能,需要在啟動配置檔案當中配置以下選項

./conf/broker.conf

enablePropertyFilter=true

啟動

start mqbroker.cmd -n 127.0.0.1:9876 -c ../conf/broker.conf

生產訊息

傳送訊息時,你能透過putUserProperty來設定訊息的屬性

public class SyncFilterProducer {
    public static void main(String[] args) throws Exception {
        // 例項化訊息生產者Producer
        DefaultMQProducer producer = new DefaultMQProducer("GroupNameDemo");
        // 設定NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        // 啟動Producer例項
        producer.start();
        // 建立訊息,並指定Topic,Tag和訊息體
        Message msg = new Message("TopicTest" /* Topic */,
                "TagA" /* Tag*/,
                ("Hello RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
        );
        // 設定屬性
        msg.putUserProperty("a","10");
        msg.putUserProperty("b","20");
        // 傳送訊息到一個Broker
        SendResult sendResult = producer.send(msg);
        // 透過sendResult返回訊息是否成功送達
        System.out.printf("%s%n", sendResult);
        // 如果不再傳送訊息,關閉Producer例項。
        producer.shutdown();
    }
}

消費訊息

用MessageSelector.bySql來使用sql篩選訊息

public class FilterConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        // 例項化消費者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("GroupNameDemo");
        // 設定NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");
        //
        consumer.subscribe("TopicTest", MessageSelector.bySql("a > 5 and b = 20"));
        // 註冊回撥實現類來處理從broker拉取回來的訊息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                // 標記該訊息已經被成功消費
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 啟動消費者例項
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

事務訊息共有三種狀態,提交狀態、回滾狀態、中間狀態:

  • TransactionStatus.CommitTransaction: 提交事務,它允許消費者消費此訊息。
  • TransactionStatus.RollbackTransaction: 回滾事務,它代表該訊息將被刪除,不允許被消費。
  • TransactionStatus.Unknown: 中間狀態,它代表需要檢查訊息佇列來確定狀態。

生產事物訊息

使用 TransactionMQProducer類建立生產者,並指定唯一的 ProducerGroup,就可以設定自定義執行緒池來處理這些檢查請求。執行本地事務後、需要根據執行結果對訊息佇列進行回覆。回傳的事務狀態在請參考前一節。

public class TransactionProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("GroupNameDemo");
        // 設定NameServer的地址
        producer.setNamesrvAddr("localhost:9876");
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("client-transaction-msg-check-thread");
                return thread;
            }
        });
        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        producer.start();
        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < 10; i++) {
            try {
                Message msg =
                        new Message("TransactionTopicTest", tags[i % tags.length], "KEY" + i,
                                ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);
                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}

實現事務的監聽介面

當傳送半訊息成功時,我們使用 executeLocalTransaction 方法來執行本地事務。它返回前一節中提到的三個事務狀態之一。checkLocalTransaction 方法用於檢查本地事務狀態,並回應訊息佇列的檢查請求。它也是返回前一節中提到的三個事務狀態之一。

public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);
    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
    /**
     * 執行本地事物
     * @param msg
     * @param arg
     * @return
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        System.out.printf("執行本地事物transactionId = %s %n",msg.getTransactionId());
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);
        return LocalTransactionState.UNKNOW;
    }

    /**
     * 回查本地事物,這裡會去檢查本地事物狀態
     * @param msg
     * @return
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.printf("檢查本地事物transactionId = %s, msgId = %s %n",msg.getTransactionId(), msg.getMsgId());
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    // 中間狀態,它代表需要檢查訊息佇列來確定狀態。
                    return LocalTransactionState.UNKNOW;
                case 1:
                    // 提交事務,它允許消費者消費此訊息。
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    // 回滾事務,它代表該訊息將被刪除,不允許被消費。
                    return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

消費事物訊息

public class Consumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        // 例項化消費者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("GroupNameDemo");
        // 設定NameServer的地址
        consumer.setNamesrvAddr("localhost:9876");
        // 訂閱一個或者多個Topic,以及Tag來過濾需要消費的訊息,多個tag之間用||分隔,* 代表所有
        consumer.subscribe("TransactionTopicTest", "*");
        // 註冊回撥實現類來處理從broker拉取回來的訊息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                // 標記該訊息已經被成功消費
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 啟動消費者例項
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

事務訊息使用上的限制

  1. 事務訊息不支援延時訊息和批次訊息。
  2. 為了避免單個訊息被檢查太多次而導致半佇列訊息累積,我們預設將單個訊息的檢查次數限制為 15 次,但是使用者可以透過 Broker 配置檔案的 transactionCheckMax引數來修改此限制。如果已經檢查某條訊息超過 N 次的話( N = transactionCheckMax ) 則 Broker 將丟棄此訊息,並在預設情況下同時列印錯誤日誌。使用者可以透過重寫 AbstractTransactionalMessageCheckListener 類來修改這個行為。
  3. 事務訊息將在 Broker 配置檔案中的引數 transactionTimeout 這樣的特定時間長度之後被檢查。當傳送事務訊息時,使用者還可以透過設定使用者屬性 CHECK_IMMUNITY_TIME_IN_SECONDS 來改變這個限制,該引數優先於 transactionTimeout 引數。
  4. 事務性訊息可能不止一次被檢查或消費。
  5. 提交給使用者的目標主題訊息可能會失敗,目前這依日誌的記錄而定。它的高可用性透過 RocketMQ 本身的高可用性機制來保證,如果希望確保事務訊息不丟失、並且事務完整性得到保證,建議使用同步的雙重寫入機制。
  6. 事務訊息的生產者 ID 不能與其他型別訊息的生產者 ID 共享。與其他型別的訊息不同,事務訊息允許反向查詢、MQ伺服器能透過它們的生產者 ID 查詢到消費者。
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章