RocketMq的使用demo

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;
    }
}
圖長度不夠畅哑,拉完的話可以看到10個消息進(jìn)入了三個隊列肴楷,4,3荠呐,3結(jié)構(gòu)赛蔫,具體是不是我們預(yù)設(shè)的那樣呢,可以消費(fèi)下看看

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.");
    }
}

查看打印結(jié)果可以發(fā)現(xiàn)同一個訂單的消息進(jìn)入了同一個隊列呵恢,并且每個隊列由一個線程進(jìn)行消費(fèi),每類訂單按push順序進(jìn)行消費(fèi)

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汽纤,特殊的常量
  • 布爾值,TRUEFALSE

只有使用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í)行流程
  1. 發(fā)送方向 MQ 服務(wù)端發(fā)送消息绢掰。
  2. MQ Server 將消息持久化成功之后,向發(fā)送方 ACK 確認(rèn)消息已經(jīng)發(fā)送成功童擎,此時消息為半消息滴劲。
  3. 發(fā)送方開始執(zhí)行本地事務(wù)邏輯。
  4. 發(fā)送方根據(jù)本地事務(wù)執(zhí)行結(jié)果向 MQ Server 提交二次確認(rèn)(Commit 或是 Rollback)顾复,MQ Server 收到
    Commit 狀態(tài)則將半消息標(biāo)記為可投遞班挖,訂閱方最終將收到該消息;MQ Server 收到 Rollback 狀態(tài)則刪除半
    消息芯砸,訂閱方將不會接受該消息萧芙。
  5. 在斷網(wǎng)或者是應(yīng)用重啟的特殊情況下给梅,上述步驟4提交的二次確認(rèn)最終未到達(dá) MQ Server,經(jīng)過固定時間后
    MQ Server 將對該消息發(fā)起消息回查双揪。
  6. 發(fā)送方收到消息回查后破喻,需要檢查對應(yīng)消息的本地事務(wù)執(zhí)行的最終結(jié)果。
  7. 發(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ù)消息使用上的限制

  1. 事務(wù)消息不支持延時消息和批量消息菇肃。
  2. 為了避免單個消息被檢查太多次而導(dǎo)致半隊列消息累積地粪,我們默認(rèn)將單個消息的檢查次數(shù)限制為 15 次,但是用戶可以通過 Broker 配置文件的 transactionCheckMax參數(shù)來修改此限制琐谤。如果已經(jīng)檢查某條消息超過 N 次的話( N = transactionCheckMax ) 則 Broker 將丟棄此消息蟆技,并在默認(rèn)情況下同時打印錯誤日志。用戶可以通過重寫 AbstractTransactionalMessageCheckListener 類來修改這個行為笑跛。
  3. 事務(wù)消息將在 Broker 配置文件中的參數(shù) transactionTimeout 這樣的特定時間長度之后被檢查付魔。當(dāng)發(fā)送事務(wù)消息時,用戶還可以通過設(shè)置用戶屬性 CHECK_IMMUNITY_TIME_IN_SECONDS 來改變這個限制飞蹂,該參數(shù)優(yōu)先于 transactionTimeout 參數(shù)几苍。
  4. 事務(wù)性消息可能不止一次被檢查或消費(fèi)。
  5. 提交給用戶的目標(biāo)主題消息可能會失敗陈哑,目前這依日志的記錄而定妻坝。它的高可用性通過 RocketMQ 本身的高可用性機(jī)制來保證伸眶,如果希望確保事務(wù)消息不丟失、并且事務(wù)完整性得到保證刽宪,建議使用同步的雙重寫入機(jī)制厘贼。
  6. 事務(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");
   }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市横殴,隨后出現(xiàn)的幾起案子被因,更是在濱河造成了極大的恐慌,老刑警劉巖衫仑,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梨与,死亡現(xiàn)場離奇詭異,居然都是意外死亡文狱,警方通過查閱死者的電腦和手機(jī)粥鞋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞄崇,“玉大人呻粹,你說我怎么就攤上這事壕曼。” “怎么了等浊?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵腮郊,是天一觀的道長。 經(jīng)常有香客問我筹燕,道長轧飞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任撒踪,我火速辦了婚禮踪少,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘糠涛。我一直安慰自己,他們只是感情好兼犯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布忍捡。 她就那樣靜靜地躺著,像睡著了一般切黔。 火紅的嫁衣襯著肌膚如雪砸脊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天纬霞,我揣著相機(jī)與錄音凌埂,去河邊找鬼。 笑死诗芜,一個胖子當(dāng)著我的面吹牛瞳抓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播伏恐,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼孩哑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了翠桦?” 一聲冷哼從身側(cè)響起横蜒,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎销凑,沒想到半個月后丛晌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡斗幼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年澎蛛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜕窿。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡瓶竭,死狀恐怖督勺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情斤贰,我是刑警寧澤智哀,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布谎痢,位于F島的核電站嗽桩,受9級特大地震影響介时,放射性物質(zhì)發(fā)生泄漏赁项。R本人自食惡果不足惜慈迈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一山上、第九天 我趴在偏房一處隱蔽的房頂上張望景图。 院中可真熱鬧固蛾,春花似錦骗爆、人聲如沸次氨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煮寡。三九已至,卻和暖如春犀呼,著一層夾襖步出監(jiān)牢的瞬間幸撕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工外臂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坐儿,地道東北人。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓宋光,卻偏偏與公主長得像貌矿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子罪佳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一.RocketMQ模型的基本概念站叼。 1 消息模型(Message Model) RocketMQ主要由 Prod...
    名字是亂打的閱讀 1,736評論 0 2
  • 官網(wǎng)網(wǎng)站: http://rocketmq.apache.org/docs/quick-start/[http:/...
    瘋狂擼代碼的奮青騷年閱讀 202評論 0 0
  • MQ(Message Queue)是一種跨進(jìn)程的通信機(jī)制,用于傳遞消息菇民。是一種高效尽楔、可靠、安全第练、可擴(kuò)展的分布式消息...
    ShawnCaffeine閱讀 823評論 0 0
  • 一阔馋、前言: LiveEventBus是一款A(yù)ndroid消息總線,基于LiveData娇掏,具有生命周期感知能力呕寝,支持...
    i小灰閱讀 2,690評論 0 2
  • Alamofire 是一款 Swift 寫的 HTTP 網(wǎng)絡(luò)請求庫 前言 本篇內(nèi)容為 Alamofire 官方 R...
    zongmumask閱讀 20,792評論 6 66