目錄
本篇博客由於是上篇博客“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,特殊的常量
- 布爾值,TRUE 或 FALSE
生產者:調用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、事務流程
上圖說明了事務消息的大致方案,其中分爲兩個流程:正常事務消息的發送及提交、事務消息的補償流程。
事務消息發送及提交
- 發送消息(half消息)
- 服務端響應消息寫入結果
- 根據發送結果執行本地事務(如果寫入失敗,此時half消息對業務不可見,本地邏輯不執行)
- 根據本地事務狀態執行Commit或者Rollback(Commit操作生成消息索引,消息對消費者可見)
事務補償
- 對沒有Commit/Rollback的事務消息(pending狀態的消息),從服務端發起一次“回查”
- Producer收到回查消息,檢查回查消息對應的本地事務的狀態
- 根據本地事務狀態,重新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消息。