ps:
本文的許多demo參考rocketmq官方文檔泽西,但是又區(qū)別于官方文檔阔加,這里提供了許多更容易的demo
依賴
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
ps 客戶端配置信息
生產(chǎn)者和消費(fèi)者都屬于MQ的客戶端,都繼承于ClientConfig類姑荷,ClientConfig為客戶端的公共配置類街望。這里將客戶端相關(guān)配置信息寫在最前面,大家可以看了就知道大概由哪些屬性了,客戶端配置
1糜俗、Producer端發(fā)送同步消息
這種可靠性同步地發(fā)送方式使用的比較廣泛踱稍,比如:重要的消息通知,短信通知悠抹。
public class SyncProducer {
public static void main(String[] args) throws Exception {
// 實例化消息生產(chǎn)者Producer
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 設(shè)置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 啟動Producer實例
producer.start();
for (int i = 0; i < 100; i++) {
// 創(chuàng)建消息珠月,并指定Topic,Tag和消息體
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 發(fā)送消息到一個Broker
SendResult sendResult = producer.send(msg);
// 通過sendResult返回消息是否成功送達(dá)
System.out.printf("%s%n", sendResult);
}
// 如果不再發(fā)送消息楔敌,關(guān)閉Producer實例啤挎。
producer.shutdown();
}
}
2、發(fā)送異步消息
異步消息通常用在對響應(yīng)時間敏感的業(yè)務(wù)場景卵凑,即發(fā)送端不能容忍長時間地等待Broker的響應(yīng)庆聘。
注意
- 異步發(fā)送在使用上其實就多了一個send時候在里面加一個回調(diào)函數(shù)的實現(xiàn)
- 由于send這里是異步請求了,如果我們還沒發(fā)送完畢就producer.shutdown();關(guān)閉實例化生產(chǎn)者會出現(xiàn)send失敗的情況,因此這里引用了countDownLatch,不需要關(guān)注什么時候結(jié)束了,這里沒看到減操作,因為是由另一端進(jìn)行減操作.如果只用簡單的TimeUnit.SECONDS.sleep(10);可能還沒push完我們的生產(chǎn)者實例已經(jīng)關(guān)閉了.
public class AsyncProducer {
public static void main(String[] args) throws Exception {
// 實例化消息生產(chǎn)者Producer
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 設(shè)置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 啟動Producer實例
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0);
int messageCount = 100;
// 根據(jù)消息數(shù)量實例化倒計時計算器
final CountDownLatch2 countDownLatch = new CountDownLatch2(messageCount);
for (int i = 0; i < messageCount; i++) {
final int index = i;
// 創(chuàng)建消息,并指定Topic氛谜,Tag和消息體
Message msg = new Message("TopicTest",
"TagA",
"OrderID188",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
// SendCallback接收異步返回結(jié)果的回調(diào)
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%-10d OK %s %n", index,
sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
System.out.printf("%-10d Exception %s %n", index, e);
e.printStackTrace();
}
});
}
// 等待5s
countDownLatch.await(5, TimeUnit.SECONDS);
// 如果不再發(fā)送消息掏觉,關(guān)閉Producer實例。
producer.shutdown();
}
}
3值漫、單向發(fā)送消息
這種方式主要用在不特別關(guān)心發(fā)送結(jié)果的場景澳腹,例如日志發(fā)送。
public class OnewayProducer {
public static void main(String[] args) throws Exception{
// 實例化消息生產(chǎn)者Producer
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// 設(shè)置NameServer的地址
producer.setNamesrvAddr("localhost:9876");
// 啟動Producer實例
producer.start();
for (int i = 0; i < 100; i++) {
// 創(chuàng)建消息杨何,并指定Topic酱塔,Tag和消息體
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 發(fā)送單向消息,沒有任何返回結(jié)果
producer.sendOneway(msg);
}
// 如果不再發(fā)送消息危虱,關(guān)閉Producer實例羊娃。
producer.shutdown();
}
}
1.3 消費(fèi)消息
注意,
- 這里用subscribe進(jìn)行訂閱,這里可以傳入多個topic或者tag,但是相互直接要用|| 和空格隔開,比如consumer.subscribe("TopicTest", "tag1 || tag2 ||tag3");
- 這里的消費(fèi)通過線程池多線程消費(fèi)的,我們可以定義消費(fèi)線程數(shù)量setConsumeThreadMax(count),setConsumeThreadMin(count),也可以定義每個線程一次拿多少消息setConsumeMessageBatchMaxSize(count)
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
// 實例化消費(fèi)者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
// 設(shè)置NameServer的地址
consumer.setNamesrvAddr("localhost:9876");
// 訂閱 ,訂閱一個或者多個Topic,以及Tag來過濾需要消費(fèi)的消息
consumer.subscribe("TopicTest", "*");
// 注冊回調(diào)實現(xiàn)類來處理從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);
// 標(biāo)記該消息已經(jīng)被成功消費(fèi)
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 啟動消費(fèi)者實例
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
2 順序消息樣例
消息有序指的是可以按照消息的發(fā)送順序來消費(fèi)(FIFO)埃跷。RocketMQ可以嚴(yán)格的保證消息有序蕊玷,可以分為分區(qū)有序或者全局有序。
順序消費(fèi)的原理解析弥雹,在默認(rèn)的情況下消息發(fā)送會采取Round Robin輪詢方式把消息發(fā)送到不同的queue(分區(qū)隊列)垃帅;而消費(fèi)消息的時候從多個queue上拉取消息,這種情況發(fā)送和消費(fèi)是不能保證順序剪勿。但是如果控制發(fā)送的順序消息只依次發(fā)送到同一個queue中贸诚,消費(fèi)的時候只從這個queue上依次拉取,則就保證了順序。當(dāng)發(fā)送和消費(fèi)參與的queue只有一個酱固,則是全局有序械念;如果多個queue參與,則為分區(qū)有序运悲,即相對每個queue龄减,消息都是有序的。
下面用訂單進(jìn)行分區(qū)有序的示例扇苞。一個訂單的順序流程是:創(chuàng)建欺殿、付款、推送鳖敷、完成脖苏。訂單號相同的消息會被先后發(fā)送到同一個隊列中,消費(fèi)時定踱,同一個OrderId獲取到的肯定是同一個隊列棍潘。
2.1 順序消息生產(chǎn)
目標(biāo)
將想按照順序消費(fèi)的消息,依次發(fā)布到統(tǒng)一個隊列中去,這里定義了三個訂單崖媚,想讓他們放三個隊列里去亦歉,并且消息按照我的消息添加順序消費(fèi)。
public class OrderMsgProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("order_topic_producer");
producer.setNamesrvAddr("1xxxxxxxx:9876");
producer.start();
// 訂單列表
List<OrderStep> orderList = new OrderMsgProducer().buildOrders();
for (OrderStep orderStep : orderList) {
Message msg = new Message("order_topic",orderStep.toString().getBytes(StandardCharsets.UTF_8));
//三種消息就落到了三個隊列上去了
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long id = (Long) arg; //根據(jù)訂單id選擇發(fā)送queue
long index = id % mqs.size();
return mqs.get((int) index);
}
}, orderStep.getOrderId());//訂單 id
}
producer.shutdown();
}
/**
* 訂單的步驟
*/
@Data
private static class OrderStep {
private long orderId;
private String type;
private String desc;
}
/**
* 生成模擬訂單數(shù)據(jù)
* 擁有不同的流程的3個訂單
*/
private List<OrderStep> buildOrders() {
List<OrderStep> orderList = new ArrayList<OrderStep>();
OrderStep orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setType("A");
orderDemo.setDesc("創(chuàng)建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setType("A");
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setType("A");
orderDemo.setDesc("推送");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111039L);
orderDemo.setType("A");
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setDesc("創(chuàng)建");
orderDemo.setType("B");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setDesc("付款");
orderDemo.setType("B");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103111065L);
orderDemo.setType("B");
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setDesc("創(chuàng)建");
orderDemo.setType("C");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setType("C");
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(15103117235L);
orderDemo.setType("C");
orderDemo.setDesc("完成");
orderList.add(orderDemo);
return orderList;
}
}
2.2 順序消費(fèi)消息
public class OrderMsgConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderTopicCustomer");
consumer.setNamesrvAddr("1xxxxxxx6");
consumer.subscribe("order_topic","*");
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
//ConsumeOrderlyStatus返回值是一個本地事務(wù)管理泥张,這里設(shè)置開啟其自動提交
context.setAutoCommit(true);
//消費(fèi)
for (MessageExt msg : msgs) {
// 可以看到每個queue有唯一的consume線程來消費(fèi), 訂單對每個queue(分區(qū))有序
System.out.println(
"consumeThread: " + Thread.currentThread().getName() +
", queueId: " + msg.getQueueId() +
", content: " + new String(msg.getBody())
);
}
try {
//模擬業(yè)務(wù)邏輯處理中...
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
System.out.println("Consumer Started.");
}
}
3 延時消息樣例
3.1 啟動消費(fèi)者等待傳入訂閱消息
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 java.util.List;
public class ScheduledMessageConsumer {
public static void main(String[] args) throws Exception {
// 實例化消費(fèi)者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer");
// 訂閱Topics
consumer.subscribe("TestTopic", "*");
// 注冊消息監(jiān)聽者
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;
}
});
// 啟動消費(fèi)者
consumer.start();
}
}
3.2 發(fā)送延時消息
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
public class ScheduledMessageProducer {
public static void main(String[] args) throws Exception {
// 實例化一個生產(chǎn)者來產(chǎn)生延時消息
DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
// 啟動生產(chǎn)者
producer.start();
int totalMessagesToSend = 100;
for (int i = 0; i < totalMessagesToSend; i++) {
Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
// 設(shè)置延時等級3,這個消息將在10s之后發(fā)送(現(xiàn)在只支持固定的幾個時間,詳看delayTimeLevel)
message.setDelayTimeLevel(3);
// 發(fā)送消息
producer.send(message);
}
// 關(guān)閉生產(chǎn)者
producer.shutdown();
}
}
3.3 驗證
您將會看到消息的消費(fèi)比存儲時間晚10秒媚创。
3.4 延時消息的使用場景
比如電商里渗钉,提交了一個訂單就可以發(fā)送一個延時消息,1h后去檢查這個訂單的狀態(tài)钞钙,如果還是未付款就取消訂單釋放庫存鳄橘。
3.5 延時消息的使用限制
// 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";
現(xiàn)在RocketMq并不支持任意時間的延時,需要設(shè)置幾個固定的延時等級芒炼,從1s到2h分別對應(yīng)著等級1到18 消息消費(fèi)失敗會進(jìn)入延時消息隊列瘫怜,消息發(fā)送時間與設(shè)置的延時等級和重試次數(shù)有關(guān),詳見代碼SendMessageProcessor.java
4 批量消息樣例
批量發(fā)送消息能顯著提高傳遞小消息的性能焕议。限制是這些批量消息應(yīng)該有相同的topic,相同的waitStoreMsgOK,而且不能是延時消息盅安。此外唤锉,這一批消息的總大小不應(yīng)超過4MB。
4.1 發(fā)送批量消息
如果您每次只發(fā)送不超過4MB的消息别瞭,則很容易使用批處理窿祥,樣例如下:
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes()));
try {
producer.send(messages);
} catch (Exception e) {
e.printStackTrace();
//處理error
}
4.2 消息列表分割
復(fù)雜度只有當(dāng)你發(fā)送大批量時才會增長,你可能不確定它是否超過了大小限制(4MB)蝙寨。這時候你最好把你的消息列表分割一下:
public class ListSplitter implements Iterator<List<Message>> {
private final int SIZE_LIMIT = 1024 * 1024 * 4;
private final List<Message> messages;
private int currIndex;
public ListSplitter(List<Message> messages) {
this.messages = messages;
}
@Override public boolean hasNext() {
return currIndex < messages.size();
}
@Override public List<Message> next() {
int startIndex = getStartIndex();
int nextIndex = startIndex;
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
Message message = messages.get(nextIndex);
int tmpSize = calcMessageSize(message);
if (tmpSize + totalSize > SIZE_LIMIT) {
break;
} else {
totalSize += tmpSize;
}
}
List<Message> subList = messages.subList(startIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
private int getStartIndex() {
Message currMessage = messages.get(currIndex);
int tmpSize = calcMessageSize(currMessage);
while(tmpSize > SIZE_LIMIT) {
currIndex += 1;
Message message = messages.get(curIndex);
tmpSize = calcMessageSize(message);
}
return currIndex;
}
private int calcMessageSize(Message message) {
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();
}
tmpSize = tmpSize + 20; // 增加?日志的開銷20字節(jié)
return tmpSize;
}
}
//把大的消息分裂成若干個小的消息
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
try {
List<Message> listItem = splitter.next();
producer.send(listItem);
} catch (Exception e) {
e.printStackTrace();
//處理error
}
}
5 過濾消息樣例
在大多數(shù)情況下晒衩,TAG是一個簡單而有用的設(shè)計,其可以來選擇您想要的消息墙歪。例如:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
消費(fèi)者將接收包含TAGA或TAGB或TAGC的消息听系。但是限制是一個消息只能有一個標(biāo)簽,這對于復(fù)雜的場景可能不起作用虹菲。在這種情況下靠胜,可以使用SQL表達(dá)式篩選消息。SQL特性可以通過發(fā)送消息時的屬性來進(jìn)行計算毕源。在RocketMQ定義的語法下浪漠,可以實現(xiàn)一些簡單的邏輯。下面是一個例子:
------------
| 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 |
------------
5.1 基本語法
RocketMQ只定義了一些基本語法來支持這個特性霎褐。你也可以很容易地擴(kuò)展它址愿。
- 數(shù)值比較,比如:>冻璃,>=响谓,<,<=俱饿,BETWEEN歌粥,=;
- 字符比較拍埠,比如:=失驶,<>,IN枣购;
- IS NULL 或者 IS NOT NULL嬉探;
- 邏輯符號 AND,OR棉圈,NOT涩堤;
常量支持類型為:
- 數(shù)值,比如:123分瘾,3.1415胎围;
- 字符,比如:'abc',必須用單引號包裹起來白魂;
- NULL汽纤,特殊的常量
- 布爾值,TRUE 或 FALSE
只有使用push模式的消費(fèi)者才能用使用SQL92標(biāo)準(zhǔn)的sql語句福荸,接口如下:
public void subscribe(finalString topic, final MessageSelector messageSelector)
5.2 使用樣例
1蕴坪、生產(chǎn)者樣例
發(fā)送消息時,你能通過putUserProperty
來設(shè)置消息的屬性
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();
Message msg = new Message("TopicTest",
tag,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 設(shè)置一些屬性
msg.putUserProperty("a", String.valueOf(i));
SendResult sendResult = producer.send(msg);
producer.shutdown();
2敬锐、消費(fèi)者樣例
用MessageSelector.bySql來使用sql篩選消息
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
// 只有訂閱的消息有這個屬性a, a >=0 and a <= 3
consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
6 消息事務(wù)樣例
事務(wù)消息共有三種狀態(tài)背传,提交狀態(tài)、回滾狀態(tài)台夺、中間狀態(tài):
- TransactionStatus.CommitTransaction: 提交事務(wù)径玖,它允許消費(fèi)者消費(fèi)此消息。
- TransactionStatus.RollbackTransaction: 回滾事務(wù)谒养,它代表該消息將被刪除挺狰,不允許被消費(fèi)。
- TransactionStatus.Unknown: 中間狀態(tài)买窟,它代表需要檢查消息隊列來確定狀態(tài)丰泊。
另外這其中
Half(Prepare) Message
指的是暫不能投遞的消息,發(fā)送方已經(jīng)將消息成功發(fā)送到了 MQ 服務(wù)端始绍,但是服務(wù)端未收到生產(chǎn)者對該消息的二次
確認(rèn)瞳购,此時該消息被標(biāo)記成“暫不能投遞”狀態(tài),處于該種狀態(tài)下的消息即半消息亏推。-
Message Status Check
由于網(wǎng)絡(luò)閃斷学赛、生產(chǎn)者應(yīng)用重啟等原因,導(dǎo)致某條事務(wù)消息的二次確認(rèn)丟失吞杭,MQ 服務(wù)端通過掃描發(fā)現(xiàn)某條消息長
期處于“半消息”時盏浇,需要主動向消息生產(chǎn)者詢問該消息的最終狀態(tài)(Commit 或是 Rollback),該過程即消息回
查芽狗。
事務(wù)消息的執(zhí)行流程
- 發(fā)送方向 MQ 服務(wù)端發(fā)送消息绢掰。
- MQ Server 將消息持久化成功之后,向發(fā)送方 ACK 確認(rèn)消息已經(jīng)發(fā)送成功童擎,此時消息為半消息滴劲。
- 發(fā)送方開始執(zhí)行本地事務(wù)邏輯。
- 發(fā)送方根據(jù)本地事務(wù)執(zhí)行結(jié)果向 MQ Server 提交二次確認(rèn)(Commit 或是 Rollback)顾复,MQ Server 收到
Commit 狀態(tài)則將半消息標(biāo)記為可投遞班挖,訂閱方最終將收到該消息;MQ Server 收到 Rollback 狀態(tài)則刪除半
消息芯砸,訂閱方將不會接受該消息萧芙。 - 在斷網(wǎng)或者是應(yīng)用重啟的特殊情況下给梅,上述步驟4提交的二次確認(rèn)最終未到達(dá) MQ Server,經(jīng)過固定時間后
MQ Server 將對該消息發(fā)起消息回查双揪。 - 發(fā)送方收到消息回查后破喻,需要檢查對應(yīng)消息的本地事務(wù)執(zhí)行的最終結(jié)果。
- 發(fā)送方根據(jù)檢查得到的本地事務(wù)的最終狀態(tài)再次提交二次確認(rèn)盟榴,MQ Server 仍按照步驟4對半消息進(jìn)行操作。
6.1 發(fā)送事務(wù)消息樣例
事務(wù)消息不涉及消費(fèi)者婴噩,因為成功或者失敗全由成產(chǎn)者進(jìn)行控制擎场。
1、創(chuàng)建事務(wù)性生產(chǎn)者
使用 TransactionMQProducer
類創(chuàng)建生產(chǎn)者几莽,并指定唯一的 ProducerGroup
迅办,就可以設(shè)置自定義線程池來處理這些檢查請求。執(zhí)行本地事務(wù)后章蚣、需要根據(jù)執(zhí)行結(jié)果對消息隊列進(jìn)行回復(fù)站欺。回傳的事務(wù)狀態(tài)在請參考前一節(jié)纤垂。
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 java.util.List;
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
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("TopicTest1234", 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();
}
}
2矾策、實現(xiàn)事務(wù)的監(jiān)聽接口--用于設(shè)定什么情況成功,失敗
當(dāng)發(fā)送半消息成功時峭沦,我們使用 executeLocalTransaction
方法來執(zhí)行本地事務(wù)贾虽。它返回前一節(jié)中提到的三個事務(wù)狀態(tài)之一。checkLocalTransaction
方法用于檢查本地事務(wù)狀態(tài)吼鱼,并回應(yīng)消息隊列的檢查請求蓬豁。它也是返回前一節(jié)中提到的三個事務(wù)狀態(tài)之一。
public class TransactionListenerImpl implements TransactionListener {
private AtomicInteger transactionIndex = new AtomicInteger(0);
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
int value = transactionIndex.getAndIncrement();
int status = value % 3;
localTrans.put(msg.getTransactionId(), status);
return LocalTransactionState.UNKNOW;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
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;
}
}
6.2 事務(wù)消息使用上的限制
- 事務(wù)消息不支持延時消息和批量消息菇肃。
-
為了避免單個消息被檢查太多次而導(dǎo)致半隊列消息累積地粪,我們默認(rèn)將單個消息的檢查次數(shù)限制為 15 次,但是用戶可以通過 Broker 配置文件的
transactionCheckMax
參數(shù)來修改此限制琐谤。如果已經(jīng)檢查某條消息超過 N 次的話( N =transactionCheckMax
) 則 Broker 將丟棄此消息蟆技,并在默認(rèn)情況下同時打印錯誤日志。用戶可以通過重寫AbstractTransactionalMessageCheckListener
類來修改這個行為笑跛。 - 事務(wù)消息將在 Broker 配置文件中的參數(shù) transactionTimeout 這樣的特定時間長度之后被檢查付魔。當(dāng)發(fā)送事務(wù)消息時,用戶還可以通過設(shè)置用戶屬性 CHECK_IMMUNITY_TIME_IN_SECONDS 來改變這個限制飞蹂,該參數(shù)優(yōu)先于
transactionTimeout
參數(shù)几苍。 - 事務(wù)性消息可能不止一次被檢查或消費(fèi)。
- 提交給用戶的目標(biāo)主題消息可能會失敗陈哑,目前這依日志的記錄而定妻坝。它的高可用性通過 RocketMQ 本身的高可用性機(jī)制來保證伸眶,如果希望確保事務(wù)消息不丟失、并且事務(wù)完整性得到保證刽宪,建議使用同步的雙重寫入機(jī)制厘贼。
- 事務(wù)消息的生產(chǎn)者 ID 不能與其他類型消息的生產(chǎn)者 ID 共享。與其他類型的消息不同圣拄,事務(wù)消息允許反向查詢嘴秸、MQ服務(wù)器能通過它們的生產(chǎn)者 ID 查詢到消費(fèi)者。
7 Logappender樣例
RocketMQ日志提供log4j庇谆、log4j2和logback日志框架作為業(yè)務(wù)應(yīng)用岳掐,下面是配置樣例
7.1 log4j樣例
按下面樣例使用log4j屬性配置
log4j.appender.mq=org.apache.rocketmq.logappender.log4j.RocketmqLog4jAppender
log4j.appender.mq.Tag=yourTag
log4j.appender.mq.Topic=yourLogTopic
log4j.appender.mq.ProducerGroup=yourLogGroup
log4j.appender.mq.NameServerAddress=yourRocketmqNameserverAddress
log4j.appender.mq.layout=org.apache.log4j.PatternLayout
log4j.appender.mq.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-4r [%t] (%F:%L) %-5p - %m%n
按下面樣例使用log4j xml配置來使用異步添加日志
<appender name="mqAppender1"class="org.apache.rocketmq.logappender.log4j.RocketmqLog4jAppender">
<param name="Tag" value="yourTag" />
<param name="Topic" value="yourLogTopic" />
<param name="ProducerGroup" value="yourLogGroup" />
<param name="NameServerAddress" value="yourRocketmqNameserverAddress"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss}-%p %t %c - %m%n" />
</layout>
</appender>
<appender name="mqAsyncAppender1"class="org.apache.log4j.AsyncAppender">
<param name="BufferSize" value="1024" />
<param name="Blocking" value="false" />
<appender-ref ref="mqAppender1"/>
</appender>
7.2 log4j2樣例
用log4j2時,配置如下饭耳,如果想要非阻塞串述,只需要使用異步添加引用即可
<RocketMQ name="rocketmqAppender" producerGroup="yourLogGroup" nameServerAddress="yourRocketmqNameserverAddress"
topic="yourLogTopic" tag="yourTag">
<PatternLayout pattern="%d [%p] hahahah %c %m%n"/>
</RocketMQ>
7.3 logback樣例
<appender name="mqAppender1"class="org.apache.rocketmq.logappender.logback.RocketmqLogbackAppender">
<tag>yourTag</tag>
<topic>yourLogTopic</topic>
<producerGroup>yourLogGroup</producerGroup>
<nameServerAddress>yourRocketmqNameserverAddress</nameServerAddress>
<layout>
<pattern>%date %p %t - %m%n</pattern>
</layout>
</appender>
<appender name="mqAsyncAppender1"class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<discardingThreshold>80</discardingThreshold>
<maxFlushTime>2000</maxFlushTime>
<neverBlock>true</neverBlock>
<appender-ref ref="mqAppender1"/>
</appender>
8 OpenMessaging樣例
OpenMessaging旨在建立消息和流處理規(guī)范,以為金融寞肖、電子商務(wù)纲酗、物聯(lián)網(wǎng)和大數(shù)據(jù)領(lǐng)域提供通用框架及工業(yè)級指導(dǎo)方案。在分布式異構(gòu)環(huán)境中新蟆,設(shè)計原則是面向云觅赊、簡單、靈活和獨(dú)立于語言琼稻。符合這些規(guī)范將幫助企業(yè)方便的開發(fā)跨平臺和操作系統(tǒng)的異構(gòu)消息傳遞應(yīng)用程序茉兰。提供了openmessaging-api 0.3.0-alpha的部分實現(xiàn),下面的示例演示如何基于OpenMessaging訪問RocketMQ欣簇。
8.1 OMSProducer樣例
下面的示例演示如何在同步规脸、異步或單向傳輸中向RocketMQ代理發(fā)送消息。
import io.openmessaging.Future;
import io.openmessaging.FutureListener;
import io.openmessaging.Message;
import io.openmessaging.MessagingAccessPoint;
import io.openmessaging.OMS;
import io.openmessaging.producer.Producer;
import io.openmessaging.producer.SendResult;
import java.nio.charset.Charset;
import java.util.concurrent.CountDownLatch;
public class SimpleProducer {
public static void main(String[] args) {
final MessagingAccessPoint messagingAccessPoint =
OMS.getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default");
final Producer producer = messagingAccessPoint.createProducer();
messagingAccessPoint.startup();
System.out.printf("MessagingAccessPoint startup OK%n");
producer.startup();
System.out.printf("Producer startup OK%n");
{
Message message = producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8")));
SendResult sendResult = producer.send(message);
//final Void aVoid = result.get(3000L);
System.out.printf("Send async message OK, msgId: %s%n", sendResult.messageId());
}
final CountDownLatch countDownLatch = new CountDownLatch(1);
{
final Future<SendResult> result = producer.sendAsync(producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8"))));
result.addListener(new FutureListener<SendResult>() {
@Override
public void operationComplete(Future<SendResult> future) {
if (future.getThrowable() != null) {
System.out.printf("Send async message Failed, error: %s%n", future.getThrowable().getMessage());
} else {
System.out.printf("Send async message OK, msgId: %s%n", future.get().messageId());
}
countDownLatch.countDown();
}
});
}
{
producer.sendOneway(producer.createBytesMessage("OMS_HELLO_TOPIC", "OMS_HELLO_BODY".getBytes(Charset.forName("UTF-8"))));
System.out.printf("Send oneway message OK%n");
}
try {
countDownLatch.await();
Thread.sleep(500); // 等一些時間來發(fā)送消息
} catch (InterruptedException ignore) {
}
producer.shutdown();
}
}
8.2 OMSPullConsumer
用OMS PullConsumer 來從指定的隊列中拉取消息
import io.openmessaging.Message;
import io.openmessaging.MessagingAccessPoint;
import io.openmessaging.OMS;
import io.openmessaging.OMSBuiltinKeys;
import io.openmessaging.consumer.PullConsumer;
import io.openmessaging.producer.Producer;
import io.openmessaging.producer.SendResult;
public class SimplePullConsumer {
public static void main(String[] args) {
final MessagingAccessPoint messagingAccessPoint =
OMS.getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default");
messagingAccessPoint.startup();
final Producer producer = messagingAccessPoint.createProducer();
final PullConsumer consumer = messagingAccessPoint.createPullConsumer(
OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "OMS_CONSUMER"));
messagingAccessPoint.startup();
System.out.printf("MessagingAccessPoint startup OK%n");
final String queueName = "TopicTest";
producer.startup();
Message msg = producer.createBytesMessage(queueName, "Hello Open Messaging".getBytes());
SendResult sendResult = producer.send(msg);
System.out.printf("Send Message OK. MsgId: %s%n", sendResult.messageId());
producer.shutdown();
consumer.attachQueue(queueName);
consumer.startup();
System.out.printf("Consumer startup OK%n");
// 運(yùn)行直到發(fā)現(xiàn)一個消息被發(fā)送了
boolean stop = false;
while (!stop) {
Message message = consumer.receive();
if (message != null) {
String msgId = message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID);
System.out.printf("Received one message: %s%n", msgId);
consumer.ack(msgId);
if (!stop) {
stop = msgId.equalsIgnoreCase(sendResult.messageId());
}
} else {
System.out.printf("Return without any message%n");
}
}
consumer.shutdown();
messagingAccessPoint.shutdown();
}
}
8.3 OMSPushConsumer
以下示范如何將 OMS PushConsumer 添加到指定的隊列熊咽,并通過 MessageListener 消費(fèi)這些消息莫鸭。
import io.openmessaging.Message;
import io.openmessaging.MessagingAccessPoint;
import io.openmessaging.OMS;
import io.openmessaging.OMSBuiltinKeys;
import io.openmessaging.consumer.MessageListener;
import io.openmessaging.consumer.PushConsumer;
public class SimplePushConsumer {
public static void main(String[] args) {
final MessagingAccessPoint messagingAccessPoint = OMS
.getMessagingAccessPoint("oms:rocketmq://localhost:9876/default:default");
final PushConsumer consumer = messagingAccessPoint.
createPushConsumer(OMS.newKeyValue().put(OMSBuiltinKeys.CONSUMER_ID, "OMS_CONSUMER"));
messagingAccessPoint.startup();
System.out.printf("MessagingAccessPoint startup OK%n");
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
consumer.shutdown();
messagingAccessPoint.shutdown();
}
}));
consumer.attachQueue("OMS_HELLO_TOPIC", new MessageListener() {
@Override
public void onReceived(Message message, Context context) {
System.out.printf("Received one message: %s%n", message.sysHeaders().getString(Message.BuiltinKeys.MESSAGE_ID));
context.ack();
}
});
consumer.startup();
System.out.printf("Consumer startup OK%n");
}
}