rocketmq 使用學習

什么是rocketmq


RocketMQ 是阿里巴巴開源的消息隊列中間件。具有下列特點:

  • 能夠保證嚴格的消息順序
  • 提供豐富的消息拉取模式
  • 高效的訂閱者水平擴展能力
  • 億級消息堆積能力
  • 事務消息

“嚴格的消息順序” 是指在需要的情況下,可以使 producer 發送的消息被 consumer 順序的接收; “豐富的消息拉取模式” 是指可以選擇 pull 或 push 兩種消息消費模式(但是其實都是 consumer 主動從broker 拉取消息);“訂閱者水平擴展能力” 是指可以多個 consumer 同時 subscribe 同一個隊列時,根據 consumer 是否在同一個 consumer group 來決定消息是交給所有 consumer 消費還是選擇某個 consumer 消費,可以實現 consumer 側的負載均衡;“億級消息堆積能力” 是指 broker 接收到的消息后會將其存在文件中,所以可以做到存儲大量消息,并供不同消費者重復消費。“事務消息” 是指可以用來實現最終一致性的分布式事務。

rocketmq的組成部分


RocketMQ 集群網絡拓撲圖

上圖是一個典型的 RocketMQ 網絡拓撲圖,有以下組成部分:

  • producer
  • consumer
  • Name server
  • Broker

Broker 又分為 master 和 slave,master 可以進行消息的讀寫,slave 同步 master 接收的消息,只能用來進行消息的讀取。其中:

(1) producer 為消息的生產者,為了提高寫消息的效率,同時防止單點,可以部署多個 master broker,producer 可以向不同的 broker 寫入數據。
(2)consumer 為消息的消費者,有集群模式和廣播模式兩種消費方式,還可以設置 consumer group。在集群模式下,同一條消息只會被同一個 consumer group 中的一個消費者消費,不同 consumer group 的 consumer 可以消費同一條消息;而廣播模式則是多個 consumer 都會消費到同一條消息。
(3)Name server 用來管理 broker 以及 broker 上的 topic,可以接收 Broker 的注冊、注銷請求,讓 producer 查詢 topic 下的所有 BrokerQueue,put 消息,Consumer 獲取 topic 下所有的 BrokerQueue,get 消息
(4) Broker 又分為 master 和 slave,master 可以進行消息的讀寫,slave 同步 master 接收的消息,只能用來進行消息的讀取。一個 Master 可以有多個 Slave,但一個 Slave 只能對應一個 Master,Master 與 Slave 的對應關系通過指定相同的BrokerName,不同的BrokerId來定義,BrokerId為0表示Master,非0表示Slaver。Master可以部署多個。每個Broker與Name Server 集群中的所有節點建立長連接,定時注冊 Topic 信息到所有的 NameServer。

需要注意的是 producer 和 consumer 在生產和消費消息時,都需要指定消息的 topic,當 topic 匹配時,consumer 才會消費到 producer 發送的消息,除此之外, producer 在發送消息時還可以指定消息的 tag,consumer 也可以指定自己關注哪些 tag,這樣就可以對消息的消費進行更加細粒度的控制 。

broker 中同一個 topic 又可以分為不同的 queue,consumer 在集群模式下消費時,同一個 topic 下不同的 queue 會被 分配給同一個 consumer group 中不同的 consumer,實現接收端的負載均衡,同時也為順序消息的實現提供了基礎。

在同一個broker上,所有 topic 的所有 queue 的消息,存放在一個文件里面,并且,為不同的 queue 生成了不同的 ConsumeQueue,這樣, consumer 就可以指定 topic、消息發送時間等信息,從 ConsumeQueue 中讀取消息在 commit log 中的偏移,然后再去 commit log 中讀取消息:

Broker 消息的存儲

rocketmq環境搭建與基本使用


安裝

搭建 RocketMQ 環境需要下列條件:

  • 64bit JDK 1.7+;
  • Maven 3.2.x

先從 github 獲取 RocketMQ 的源碼:

git clone https://github.com/apache/incubator-rocketmq.git

然后進入源碼目錄進行編譯:

 mvn clean package install -Prelease-all assembly:assembly -U

需要注意的是,在 Mac os x 上,有些測試無法通過,加入 -DskipTests 即可,不影響使用。在 linux 和 windows 上都沒有這個問題。

然后就可以進入 target/apache-rocketmq-all/,準備運行 name server 和 broker了。

單 broker 測試

運行 name server:

nohup sh bin/mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log

如果看到日志中出現: The Name Server boot success...,說明 name server 就啟動成功了。

運行 broker:

nohup sh bin/mqbroker -n localhost:9876 &
tail -f ~/logs/rocketmqlogs/broker.log 

看到 The broker[%s, 192.168.0.133:10911] boot success... 這樣的日志,就算啟動成功了。

然后運行 consumer,代碼如下:

        /*
         * Instantiate with specified consumer group name.
         */
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
        consumer.setNamesrvAddr("192.168.0.133:9876");

        /*
         * Specify name server addresses.
         * <p/>
         *
         * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
         * <pre>
         * {@code
         * consumer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
         * }
         * </pre>
         */

        /*
         * Specify where to start in case the specified consumer group is a brand new one.
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        /*
         * Subscribe one more more topics to consume. * represent this consumer will consume all sub tags
         */
        consumer.subscribe("TopicTest", "*");

        /*
         *  Register callback to execute on arrival of messages fetched from brokers.
         */
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        /*
         *  Launch the consumer instance.
         */
        consumer.start();

        System.out.printf("Consumer Started.%n");

再運行 producer:


        /*
         * Instantiate with a producer group name.
         */
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        producer.setNamesrvAddr("192.168.0.133:9876");

        /*
         * Specify name server addresses.
         * <p/>
         *
         * Alternatively, you may specify name server addresses via exporting environmental variable: NAMESRV_ADDR
         * <pre>
         * {@code
         * producer.setNamesrvAddr("name-server1-ip:9876;name-server2-ip:9876");
         * }
         * </pre>
         */

        /*
         * Launch the instance.
         */
        producer.start();

        for (int i = 0; i < 1000; i++) {
            try {

                /*
                 * Create a message instance, specifying topic, tag and message body.
                 */
                Message msg = new Message("TopicTest" /* Topic */,
                    "TagA" /* Tag */,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
                );

                /*
                 * Call send message to deliver message to one of brokers.
                 */
                SendResult sendResult = producer.send(msg);

                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }

        /*
         * Shut down once the producer instance is not longer in use.
         */
        producer.shutdown();

就可以看到 consumer 打印出接收到的消息了:

                ...
ConsumeMessageThread_16 Receive New Messages: [MessageExt [queueId=0, storeSize=180, queueOffset=749, sysFlag=0, bornTimestamp=1492238283708, bornHost=/192.168.0.103:50436, storeTimestamp=1492238278824, storeHost=/192.168.0.104:10911, msgId=C0A8006800002A9F0000000000091409, commitLogOffset=594953, bodyCRC=801108784, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicTest, flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=750, CONSUME_START_TIME=1492238283710, UNIQ_KEY=C0A80067C1B018B4AAC248A9BDBC03E3, WAIT=true, TAGS=TagA}, body=18]]]
                ...

RocketMQ 集群

只使用單個 Broker 單個 Name Server 的話,無法保證服務的高可用,所以一般會選擇啟動多個 NameServer,多個 Master 以及 多個 slave。可以配置的選項主要有:(1)接收到消息寫入文件后刷盤是異步還是同步,同步刷盤會導致磁盤 IO 增多從而運行效率下降,同時由于有若干 slave 備份消息,一般不建議使用同步刷盤;(2)master slave 之間復制消息使用同步還是異步方式,同步方式的情況下 producer 寫入消息后,當消息從 master 復制到 slave 成功后才返回,而異步情況下 master 處理好了消息就直接返回了。在 incubator-rocketmq/target/apache-rocketmq-all/conf 目錄下,有一些示例配置:2m-2s-async、2m-2s-sync、2m-noslave 分別對應不同的配置示例,這里就配置 2m-noslave。

有2臺服務器 192.168.0.133 以及 192.168.0.104,我們先在兩臺服務器上分別啟動 name server。

然后使用

nohup bash mqbroker -c ../conf/2m-noslave/broker-a.properties -n '192.168.0.133:9876;192.168.0.104:9876' &
nohup bash mqbroker -c ../conf/2m-noslave/broker-b.properties -n '192.168.0.133:9876;192.168.0.104:9876' &

分別啟動不同的 Broker。這里需要注意的是 Broker 的配置項和 org.apache.rocketmq.common.BrokerConfig 類的成員變量一一對應,如果有定制化的,直接看看 BrokerConfig 中有什么選項就好了。

查看 Name Server 的日志,可以看到兩個 Broker 分別在兩個 Name Server 上注冊成功。在 consumer 和 producer 中,也記得使用下面的代碼來設置 Name Server:

consumer.setNamesrvAddr("192.168.0.104:9876;192.168.0.133:9876");
producer.setNamesrvAddr("192.168.0.104:9876;192.168.0.133:9876");

順序消息


順序消息指消息被消費的順序和 producer 發送消息的順序嚴格一致。RocketMQ 要實現順序消息有 2 個要求:

  1. Producer 保證發送消息到同一個隊列;
  2. consumer 保證同一個隊列同時只有一個 consumer 在消費。

具體實現上,Producer 需要使用 MessageQueueSelector 根據業務需求使用某個參數,比如訂單號,將關聯的數據發送到同一個隊列去。

Consumer 需要使用 MessageListenerOrderly,它將會定時的向 Broker 申請鎖住某些特定的隊列,Broker 的RebalanceLockManager 里的 ConcurrentHashMap mqLockTable 記錄著隊列與 consumer client 的對應關系,consumer 可以嘗試對隊列加鎖,并獲取自己當前持有哪些隊列的鎖:

   private final ConcurrentHashMap<String/* group */, ConcurrentHashMap<MessageQueue, LockEntry>> mqLockTable =
        new ConcurrentHashMap<String, ConcurrentHashMap<MessageQueue, LockEntry>>(1024);

對于 consumer,除了知道自己持有哪些隊列的鎖,可以對這些隊列進行消費外,還需要保證同一時間只有一個線程會消費同一個隊列,所以在本地維護了一個變量,其類型為:

public class MessageQueueLock {
    private ConcurrentHashMap<MessageQueue, Object> mqLockTable =
        new ConcurrentHashMap<MessageQueue, Object>();

    public Object fetchLockObject(final MessageQueue mq) {
        Object objLock = this.mqLockTable.get(mq);
        if (null == objLock) {
            objLock = new Object();
            Object prevLock = this.mqLockTable.putIfAbsent(mq, objLock);
            if (prevLock != null) {
                objLock = prevLock;
            }
        }

        return objLock;
    }
}

對于每一個隊列,都有一個 objLock,在消費時對該 objLock 使用 synchronizd 加鎖,保證同一時間只有一個線程在消費該隊列。

對于每個正在處理中的隊列,用一個 ProcessQueue 維護其狀態,并在內部使用一個 TreeMap 記錄所有本地獲取到且未消費的消息,key 為消息的 offset,value 為消息,方便按消息的 offset 獲取消息:

    private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();

為了實現消費失敗時暫停消費,還再讀取消息進行處理時將消息放到一個暫存隊列里:

  public List<MessageExt> takeMessags(final int batchSize) {
        List<MessageExt> result = new ArrayList<MessageExt>(batchSize);
        final long now = System.currentTimeMillis();
        try {
            this.lockTreeMap.writeLock().lockInterruptibly();
            this.lastConsumeTimestamp = now;
            try {
                if (!this.msgTreeMap.isEmpty()) {
                    for (int i = 0; i < batchSize; i++) {
                        Map.Entry<Long, MessageExt> entry = this.msgTreeMap.pollFirstEntry();
                        if (entry != null) {
                            result.add(entry.getValue());
                            msgTreeMapTemp.put(entry.getKey(), entry.getValue());
                        } else {
                            break;
                        }
                    }
                }

                if (result.isEmpty()) {
                    consuming = false;
                }
            } finally {
                this.lockTreeMap.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            log.error("take Messages exception", e);
        }

        return result;
    }

這樣,就可以在處理失敗時將消息從 msgTreeMapTemp 放回 msgTreeMap 中,在成功時候增加消息消費的 offset 了:

    public void rollback() {
        try {
            this.lockTreeMap.writeLock().lockInterruptibly();
            try {
                this.msgTreeMap.putAll(this.msgTreeMapTemp);
                this.msgTreeMapTemp.clear();
            } finally {
                this.lockTreeMap.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            log.error("rollback exception", e);
        }
    }

  public long commit() {
        try {
            this.lockTreeMap.writeLock().lockInterruptibly();
            try {
                Long offset = this.msgTreeMapTemp.lastKey();
                msgCount.addAndGet(this.msgTreeMapTemp.size() * (-1));
                this.msgTreeMapTemp.clear();
                if (offset != null) {
                    return offset + 1;
                }
            } finally {
                this.lockTreeMap.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            log.error("commit exception", e);
        }

        return -1;
    }

在處理完消息后,會根據處理結果進行一些后序動作,包括增加消費的 offset,并更新 offset 到 Broker 等,這樣就不會每次隊列重啟都重新消費之前的數據了:

    public boolean processConsumeResult(//
        final List<MessageExt> msgs, //
        final ConsumeOrderlyStatus status, //
        final ConsumeOrderlyContext context, //
        final ConsumeRequest consumeRequest//
    ) {
        boolean continueConsume = true;
        long commitOffset = -1L;
        if (context.isAutoCommit()) {
            switch (status) {
                case COMMIT:
                case ROLLBACK:
                    log.warn("the message queue consume result is illegal, we think you want to ack these message {}",
                        consumeRequest.getMessageQueue());
                case SUCCESS:
                    commitOffset = consumeRequest.getProcessQueue().commit();
                    this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
                    break;
                case SUSPEND_CURRENT_QUEUE_A_MOMENT:
                    this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
                    if (checkReconsumeTimes(msgs)) {
                        consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs);
                        this.submitConsumeRequestLater(//
                            consumeRequest.getProcessQueue(), //
                            consumeRequest.getMessageQueue(), //
                            context.getSuspendCurrentQueueTimeMillis());
                        continueConsume = false;
                    } else {
                        commitOffset = consumeRequest.getProcessQueue().commit();
                    }
                    break;
                default:
                    break;
            }
        } else {
            switch (status) {
                case SUCCESS:
                    this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
                    break;
                case COMMIT:
                    commitOffset = consumeRequest.getProcessQueue().commit();
                    break;
                case ROLLBACK:
                    consumeRequest.getProcessQueue().rollback();
                    this.submitConsumeRequestLater(//
                        consumeRequest.getProcessQueue(), //
                        consumeRequest.getMessageQueue(), //
                        context.getSuspendCurrentQueueTimeMillis());
                    continueConsume = false;
                    break;
                case SUSPEND_CURRENT_QUEUE_A_MOMENT:
                    this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
                    if (checkReconsumeTimes(msgs)) {
                        consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs);
                        this.submitConsumeRequestLater(//
                            consumeRequest.getProcessQueue(), //
                            consumeRequest.getMessageQueue(), //
                            context.getSuspendCurrentQueueTimeMillis());
                        continueConsume = false;
                    }
                    break;
                default:
                    break;
            }
        }

        if (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
            this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false);
        }

        return continueConsume;
    }

分析了這么多,還是上一段代碼來說明一下使用的方法,下面為 producer:

       try {
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            producer.setNamesrvAddr("192.168.0.104:9876;192.168.0.133:9876");
//            producer.setNamesrvAddr("192.168.0.104:9876");
//            producer.setNamesrvAddr("192.168.0.133:9876");
            producer.start();

            String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
            Random random = new Random();
            random.setSeed(System.currentTimeMillis());
            for (int i = 0; i < 100; i++) {
                int orderId = Math.abs(random.nextInt());
                Message msg =
                    new Message("TopicTestShunxu", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        System.out.println(mqs);
                        Integer id = (Integer) arg;
                        int index = id % mqs.size();
                        return mqs.get(index);
                    }
                }, orderId);

                System.out.printf("%s%n", sendResult);
            }

            producer.shutdown();
        } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            e.printStackTrace();
        }
    }

下面是 consumer:

  DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");

        consumer.setNamesrvAddr("192.168.0.104:9876;192.168.0.133:9876");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        consumer.subscribe("TopicTestShunxu", "TagA || TagC || TagD");

        consumer.registerMessageListener(new MessageListenerOrderly() {
            AtomicLong consumeTimes = new AtomicLong(0);

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                this.consumeTimes.incrementAndGet();
                for (MessageExt msg : msgs) {
                    System.out.println(msg.getStoreHost() + " " + msg.getQueueId() + " " + new String(msg.getBody(), Charset.forName("UTF-8")));
                }

                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("Consumer Started.%n");

注意,對于 consumer 而言,在暫時無法成功處理消息時,需要返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT,這樣就會在一段時間之后重試消費消息。

另外還有一點要注意的是,順序消息不能保證消息只被消費一次:比如當某個 consumer 處理完消息但還沒有更新消息 offset 到 broker 時掛了,其他的 consumer 會獲取隊列的鎖,并且重新消費該消息。所以在 consumer 的業務邏輯中一定一定要對消息做去重處理,否則要是發了兩份貨或者轉了兩筆錢,你老板可能就會扣你工資了 ??。

事務消息


所謂的事務消息,是指將事務處理+消息發送結合起來,保證同時失敗或同時成功。比如從賬戶 A 扣錢,發了一個消息給賬戶 B 增加一筆錢,那么必須保證扣錢成功就發出去消息,扣錢失敗不能發出去消息。這樣做的好處是什么呢?

在單機環境下,一個轉賬操作如下:

單機事務

但是當用戶十分多以后,兩個賬戶可能不在一臺服務器上,可能需要這樣做:

集群環境下的轉賬

但是像上圖這樣做,編程會十分復雜,要考慮到各種異常情況,同時效率也比較低。那么可能會有下面的這種解決方案,將大事務分解為小事務+消息,不追求完全的一致性,只需要最終一致就好:

大事務分解為小事務

最終一致性這種處理問題的思路我們其實經常會用到,一個典型的例子就是調用通過第三方支付平臺給用戶轉賬,我們在調用其 API 進行請求時,可能會返回成功,可能會返回失敗,也可能返回未知狀態。如果直接返回了成功或失敗,就可以直接決定調用失敗或者是調用成功,減少用戶賬戶余額,但是如果返回未知,則可能需要從用戶賬戶中扣款,然后記錄用戶有一筆轉賬在進行中,后續對該轉賬進行處理,查詢其是否成功來決定完成扣款或返還金額到用戶賬戶。這里就用一個轉賬記錄實現了最終一致性。

但是這種場景有一個問題:那就是到底什么時發送消息。如果在事務完成之前發,那么事務失敗的話怎么辦?如果在事務完成之后發,那么消息發送失敗了怎么辦?當然還有一種選擇是在事務中發送消息,先不 commit 事務,在消息發送后根據消息發送結果決定是 commit 還是 rollback,但是這樣又會造成事務時間過長,可能會造成數據庫查詢效率下降。

RocketMQ 解決這個問題的方法是進行兩階段提交,在事務開始前先發送一個 prepared 消息,完成事務后再發送確認消息,之后,consumer 就可以讀取到這個消息進行消費了。但是,這又引入了一個問題,確認消息發送失敗了怎么辦?RocketMQ 是這么做的:在收到 prepared 消息而未收到確認消息的情況下,每隔一段時間向消息發送端( producer )確認,事務是否執行成功。這樣就能保證消息發送與本地事務同時成功或同時失敗。

所以,使用事務消息要提供兩種 callback:

  1. 執行事務的 callback,在執行完事務后根據執行結果發送確認消息;
  2. RocketMQ 查詢事務結果的 callback,在這個 callback 里查詢事務執行的結果。

下面,就來一個簡單的例子:

//執行事務的 callback 
public class TransactionExecuterImpl implements LocalTransactionExecuter {
    private AtomicInteger transactionIndex = new AtomicInteger(1);

    @Override
    public LocalTransactionState executeLocalTransactionBranch(final Message msg, final Object arg) {
        int value = transactionIndex.getAndIncrement();
        System.out.println("execute local transaction " + msg.toString());

        if (value == 0) {
            throw new RuntimeException("Could not find db");
        } else if ((value % 5) == 0) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        } else if ((value % 4) == 0) {
            return LocalTransactionState.COMMIT_MESSAGE;
        }

        return LocalTransactionState.UNKNOW;
    }
}
//檢查事務完成情況的 callback 比如可以在 msg 中帶上 訂單號,查詢訂單是否支付成功
public class TransactionCheckListenerImpl implements TransactionCheckListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);

    @Override
    public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
        System.out.printf("server checking TrMsg " + msg.toString() + "%n");

        int value = transactionIndex.getAndIncrement();
        if ((value % 6) == 0) {
            throw new RuntimeException("Could not find db");
        } else if ((value % 5) == 0) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        } else if ((value % 4) == 0) {
            return LocalTransactionState.COMMIT_MESSAGE;
        }

        return LocalTransactionState.UNKNOW;
    }
}

下面是 producer:

        TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer("trans_group");
        producer.setNamesrvAddr("192.168.0.133:9876");
        producer.setCheckThreadPoolMinSize(2);
        producer.setCheckThreadPoolMaxSize(2);
        producer.setCheckRequestHoldMax(2000);
        producer.setTransactionCheckListener(transactionCheckListener);
        producer.start();

        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
        for (int i = 0; i < 100; i++) {
            try {
                Message msg =
                    new Message("topicTrans", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, 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();

以上。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • 分布式開放消息系統(RocketMQ)的原理與實踐 來源:http://www.lxweimin.com/p/453...
    meng_philip123閱讀 12,994評論 6 104
  • 消息中間件需要解決哪些問題? Publish/Subscribe 發布訂閱是消息中間件的最基本功能,也是相對于傳統...
    壹點零閱讀 1,626評論 0 7
  • 簡介 近年來,淘寶天貓“雙十一”活動影響了整個中國互聯網電商的發展,在“雙十一”的背后,有一系列開放平臺技術的使用...
    MisterCH閱讀 2,669評論 1 7
  • 錦溪花園酒店坐落于江蘇省昆山市錦溪古鎮,臨近淀山湖、周莊,周邊景色優美 此民宿有三套風格,一套是以上中式高檔風格,...
    美幻堂閱讀 168評論 0 0