RocketMQ學習(四):消息類型——延時消息,批量消息,過濾消息,事務消息

目錄

延時消息

批量消息

過濾消息

1、Tag過濾

2、SQL過濾

事務消息

1、事務流程

2、代碼


本篇博客由於是上篇博客“RocketMQ學習(三):消息類型——發送方式,接收方式,順序消息”的後續,因此代碼只貼了變動部分。

延時消息

比如上傳文件,我們可以先上傳到臨時目錄,然後發送一個1h的延時消息,1h後若文件表單沒有提交,我們就刪除文件釋放存儲。

生產者:需要在發送消息之前設置延時級別,且目前RocketMQ的延時級別是預設好的,不能自定義精度。

// 可用的級別對應的時間
private String avilibleDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

Message msg = new Message("base", "tag4", ("Delay msg-" + i).getBytes());
msg.setDelayTimeLevel(2); // 級別2代表延時5s
producer.send(msg);

消費者:可以打印一下消費到消息的時間和該消息被存儲到隊列的時間差。

consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
    list.forEach(item -> {
        System.out.println(new String(item.getBody()) + "  延時:" + (System.currentTimeMillis() - item.getStoreTimestamp()) + "ms later");
    });
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});

批量消息

之前我們所有的例子的消息都是以循環的方式發送的,這樣的效率不高。批量消息的發送能夠提高小消息的傳遞能力。

注意:批量消息中的所有消息的Topic和waitStoreMsgOK必須是一樣的,不能使用延時消息,並且這一批消息的大小不能超過1M。

生產者:對於小於1M的消息,只需要將多個Message放到集合裏,一起發送即可。

List<Message> messages = new ArrayList<>();
messages.add(new Message("BatchTopic", "tag1", "hello1".getBytes()));
messages.add(new Message("BatchTopic", "tag1", "hello2".getBytes()));
messages.add(new Message("BatchTopic", "tag1", "hello3".getBytes()));
producer.send(messages);

對於超過了1M大小的一批消息,我們需要將其拆分爲一批一批小於1M的消息來發送。具體實現是,循環累加集合內的消息大小,並與1M比較,來返回小於1M的部分。

public class ListSplitter implements Iterator<List<Message>> {
    private final int SIZE_LIMIT = 1024 * 1024;
    private final List<Message> messages;
    private int currIndex;

    public ListSplitter(List<Message> messages) {
        this.messages = messages;
    }

    @Override
    public boolean hasNext() {
        return currIndex < messages.size();
    }

    /**
     * 用於返回不超過1M的消息集合
     */
    @Override
    public List<Message> next() {
        int nextIndex = currIndex;
        // 用於存放消息的總長度
        int totalSize = 0;

        // 遍歷以計算消息是否超過1M
        for (; nextIndex < messages.size(); nextIndex++) {
            Message message = messages.get(nextIndex);

            // 取Topic和消息內容的大小
            int tmpSize = message.getTopic().length() + message.getBody().length;

            // 取額外屬性的大小
            Map<String, String> properties = message.getProperties();
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                tmpSize += entry.getKey().length() + entry.getValue().length();
            }

            // 增加日誌的開銷20字節
            tmpSize = tmpSize + 20;

            // 單條消息是否超過1M
            if (tmpSize > SIZE_LIMIT) {

                // 假如當前消息是next()方法的第一次遍歷, 則單獨返回此消息,否則返回之前已經過遍歷的消息
                if (nextIndex - currIndex == 0) {
                    nextIndex++;
                }
                break;
            }

            // 目前遍歷到的所有消息的總大小是否超過1M,超過就返回未超過部分,未超過就加上,進行下一次循環
            if (tmpSize + totalSize > SIZE_LIMIT) {
                break;
            } else {
                totalSize += tmpSize;
            }

        }

        // 返回的是不超過1M的消息集合
        List<Message> subList = messages.subList(currIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }
}

生產者只需要將消息集合傳遞給這個迭代器即可。

List<Message> messages = new ArrayList<>();
// 添加若干消息.....

// 把大的消息分裂成若干個小的消息
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
    try {
        List<Message> listItem = splitter.next();
        producer.send(listItem);
    } catch (Exception e) {
        e.printStackTrace();
        //處理error
    }
}

過濾消息

1、Tag過濾

在大多數情況下,我們都能使用Tag來過濾消息。

consumer.subscribe("BatchTopic", "tag1");
consumer.subscribe("BatchTopic", "tag1 || tag2");
consumer.subscribe("BatchTopic", "*");

通過不同消息的不同標籤實現的過濾方便,但一個消息只能有一個標籤,這意味着判斷依據只有一個,這對於複雜場景顯得有些力不從心。

2、SQL過濾

RocketMQ的SQL過濾功能,能夠在發送消息時附帶一些屬性,在消費者獲取時進行一些計算,來篩選消息。

需要配置:enablePropertyFilter = true

RocketMQ只定義了一些基礎語法來支持這個特性,你也可以很容易的擴展它。

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

支持的常量類型:

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

生產者:調用putUserProperty添加屬性

for (int i = 0; i < 10; i++) {
    Message message = new Message("Filter", "tag1", ("hello" + i).getBytes());
    // 添加一個屬性
    message.putUserProperty("key", String.valueOf(i));
    producer.send(message);
}

消費者:在訂閱消息時,設置過濾條件

// 只獲取key在0-4的消息
consumer.subscribe("Filter", MessageSelector.bySql("key between 0 and 4"));

可以看到,消費者只接收了5個消息(key:0-4)

事務消息

1、事務流程

上圖說明了事務消息的大致方案,其中分爲兩個流程:正常事務消息的發送及提交、事務消息的補償流程。

事務消息發送及提交

  1. 發送消息(half消息)
  2.  服務端響應消息寫入結果
  3. 根據發送結果執行本地事務(如果寫入失敗,此時half消息對業務不可見,本地邏輯不執行)
  4. 根據本地事務狀態執行Commit或者Rollback(Commit操作生成消息索引,消息對消費者可見)

事務補償

  1. 對沒有Commit/Rollback的事務消息(pending狀態的消息),從服務端發起一次“回查”
  2. Producer收到回查消息,檢查回查消息對應的本地事務的狀態
  3. 根據本地事務狀態,重新Commit或者Rollback

其中,補償階段用於解決消息Commit或者Rollback發生超時或者失敗的情況。

事務消息狀態

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

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

注意事項:要使用集羣實現事務消息,集羣必須是異步的2m-2s-async

2、代碼

生產者需要使用TransactionMQProducer來創建生產者,然後設置本地事務的監聽器用於處理本地事務,發送消息時使用sendMessageInTransaction方法。消費者不需要變動。

public class TransactionProducer {
    public static void main(String[] args) throws Exception {
        TransactionMQProducer producer = new TransactionMQProducer("group1");
        producer.setNamesrvAddr("192.168.1.1:9876");

        // 設置事務消息的監聽器
        producer.setTransactionListener(new TransactionListener() {
            /**
             * 執行本地事務
             */
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                // 根據不同tag做不同操作
                if ("TagA".equals(message.getTags())) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                } else if ("TagB".equals(message.getTags())) {
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                } else if ("TagC".equals(message.getTags())) {
                    return LocalTransactionState.UNKNOW;
                }
                return LocalTransactionState.UNKNOW;
            }

            /**
             * 本地事務的回查,UNKNOW狀態的消息回調這個方法
             */
            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                System.out.println("MQ檢查消息Tag【"+messageExt.getTags()+"】的本地事務執行結果");
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });
        producer.start();

        String[] tags = {"TagA", "TagB", "TagC"};

        for (int i = 0; i < 10; i++) {
            String tag = tags[i % 3];
            Message message = new Message("Transaction", tag, (tag + "消息" + i).getBytes());
            // 此處的第二個參數會傳遞到executeLocalTransaction()方法的第二個參數去
            producer.sendMessageInTransaction(message, null);
        }

        // 不關閉生產者的原因是其要監聽回傳
//        TimeUnit.SECONDS.sleep(5);
//        producer.shutdown();
    }
}

我們可以看到,只有TagC的消息進入了本地事務的回查,消費者端,只接收了一開始被提交的TagA消息,和回查後提交的TagC消息。

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