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包含了以下幾個重要子模組。
- Remoting Module:整個Broker的實體,負責處理來自clients端的請求。
- Client Manager:負責管理客戶端(Producer/Consumer)和維護Consumer的Topic訂閱資訊
- Store Service:提供方便簡單的API介面處理訊息儲存到物理硬碟和查詢功能。
- HA Service:高可用服務,提供Master Broker 和 Slave Broker之間的資料同步功能。
- 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安裝
- 下載
https://mirror.bit.edu.cn/apache/rocketmq/4.7.0/rocketmq-all-4.7.0-bin-release.zip
解壓
配置RocketMQ環境變數,ROCKETMQ_HOME=rocketmq解壓目錄
啟動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
啟動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,特殊的常量
- 布林值,TRUE 或 FALSE
只有使用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");
}
}
事務訊息使用上的限制
- 事務訊息不支援延時訊息和批次訊息。
- 為了避免單個訊息被檢查太多次而導致半佇列訊息累積,我們預設將單個訊息的檢查次數限制為 15 次,但是使用者可以透過 Broker 配置檔案的
transactionCheckMax
引數來修改此限制。如果已經檢查某條訊息超過 N 次的話( N =transactionCheckMax
) 則 Broker 將丟棄此訊息,並在預設情況下同時列印錯誤日誌。使用者可以透過重寫AbstractTransactionalMessageCheckListener
類來修改這個行為。 - 事務訊息將在 Broker 配置檔案中的引數 transactionTimeout 這樣的特定時間長度之後被檢查。當傳送事務訊息時,使用者還可以透過設定使用者屬性 CHECK_IMMUNITY_TIME_IN_SECONDS 來改變這個限制,該引數優先於
transactionTimeout
引數。 - 事務性訊息可能不止一次被檢查或消費。
- 提交給使用者的目標主題訊息可能會失敗,目前這依日誌的記錄而定。它的高可用性透過 RocketMQ 本身的高可用性機制來保證,如果希望確保事務訊息不丟失、並且事務完整性得到保證,建議使用同步的雙重寫入機制。
- 事務訊息的生產者 ID 不能與其他型別訊息的生產者 ID 共享。與其他型別的訊息不同,事務訊息允許反向查詢、MQ伺服器能透過它們的生產者 ID 查詢到消費者。
本作品採用《CC 協議》,轉載必須註明作者和本文連結