關於RocketMQ的一些基礎知識點及使用建議

之前一段時間團隊中引入了RocketMQ以作爲RabbitMQ的替代品, 一者是讓使用它的較高吞吐量,其次想在其上做一些擴展開發,消息事務也是我們想用它的原因之一;

MQ相關介紹

消息隊列是一種可以實現系統異步通信的中間件,常用於解決系統異步解耦和請求(TPS)削峯填谷的問題。即它面向的是開發人員,而非終端用戶可以直接使用的產品;

實現一個MQ產品所常用的三種消息協議

  1. JMS (Java Message Service)
    JMS 本質上是 JAVA API。在 JMS 中定義了 Producer,Consumer,Provider 三種角色,Producer 作爲消息的發送方,Consumer 作爲消息的接收方,Provider 作爲服務的提供者,Producer 和 Consumer 統稱爲 Client。JMS 定義了點對點和發佈訂閱兩種消息模型,發佈訂閱模型中,通過 topic 對消息進行路由,生產者可以將消息發到指定的 topic,消費者訂閱這個 topic 即可收到生產者發送的消息。
    一個生產者可以向一個或多個 topic 中發送消息,一個消費者也可以消費一個或多個 topic 中的消息,一個 topic 也可以有多個生產者或消費者,生產者和消費者只需要關聯 topic,而不用關心這消息由誰發送或者消費。 Provider 爲每一個 topic 維護一個或多個 queue 來保存消息,消息在 queue 中是有序的,遵循先進先出的原則,不同 queue 間的消息是無序的。點對點模式中沒有 topic 的概念,生產者直接將消息發送到指定 queue,消費者也指定 queue 進行消費,消息只能被一個消費者消費,不可以被多個消費者消費。Kafka 和 RocketMQ 都實現了或部分實現了 JMS 協議。

  2. AMQP(Advanced Message Queuing Protocol)
    與 JMS 不同,AMQP 是一個應用層的網絡傳輸協議,對報文格式進行定義,與開發語言無關。在 AMQP 中同樣有生產者,消費者兩種角色,消息也是保存在 queue 中的。 但不同於 JMS 用 topic 對消息進行路由,AMQP 的路由方式由 exchange 和 binding 決定。
    client 可以創建 queue,並在創建 queue 的同時通知 exchange 這個 queue 接受符合什麼條件的消息,這個條件即爲 Bingding key。生產者發送消息到 exchange 的時候會指定一個 router key,exchange 收到消息後會與自己所維護的 Bingding key 做比較,發送到符合條件的 queue 中。消費者在消費時指定 queue 進行消費。RabbitMQ 實現了 AMQP 協議。

  3. MQTT(Message Queuing Telemetry Transport)
    MQTT 協議是一種基於發佈訂閱的輕量級協議,支持 TCP 和 UDP 兩種連接方式,主要應用於即時通訊,小型設備,移動應用等領域。 MQTT 中有發佈者(Publish),訂閱者(Subscribe)和代理服務器(Broker)三種角色。Broker 是服務的提供者,發佈者和前兩種協議中的生產者相同,將消息(Message)發送到 Broker,Subscribe 從 Broker 中獲取消息並做業務處理。
    MQTT 的 Message 中固定消息頭(Fixed header)僅有 2 字節,開銷極小,除此之外分爲可變頭(Variable header)和消息體(payload)兩部分。固定頭中包含消息類型,消息級別,變長頭的大小以及消息體的總長度等信息。 變長頭則根據消息類別,含有不同的標識信息。 MQTT 允許客戶端動態的創建主題,發佈者與服務端建立會話(session)後,可以通過 Publish 方法發送數據到服務端的對應主題,訂閱者通過 Subscribe 訂閱主題後,服務端就會將主題中的消息推送給對應的訂閱者。

RocketMQ架構設計

RocketMQ簡介

Apache RocketMQ是一個分佈式消息及流處理組件,具有低延遲,高性能,萬億級容量及靈活擴展等特性;

系統模塊劃分

這裏借用RocketMQ官網的一張圖
RocketMQ architecture

  • NameServer Cluster
    名稱服務器提供輕量級服務發現和路由。 每個名稱服務器記錄完整的路由信息,提供相應的讀寫服務,並支持快速存儲擴展。
  • Broker Cluster
    中轉者通過提供輕量級的TOPIC和QUEUE機制來處理消息存儲。它們支持Push和Pull模型,包含容錯機制(2個副本或3個副本),並提供強大的峯值填充和按原始時間順序累積數千億條消息的能力。此外,Brokers還提供災難恢復,豐富的指標統計和警報機制,所有這些都是傳統的消息中間件所欠缺的。
  • Producer Cluster
    生產者支持分佈式部署。 Distributed Producers通過多種負載均衡模式向Broker集羣發送消息。 發送過程支持失敗快速重試並具有低延遲。
  • Consumer Cluster
    消費者也支持Push和Pull模型中的分佈式部署。 它還支持羣集消費和消息廣播。 它提供實時消息訂閱機制,可以滿足大多數消費者的需求。 RocketMQ的網站爲感興趣的用戶提供了一個簡單的快速入門指南。

RocketMQ相關使用

單機環境

一個NameServer, 一個Broker, 一個Producer, 一個或多個Consumer

啓動一個生產者

    @Bean(name = "defaultMQProducer")
    public DefaultMQProducer buildDefaultMQProducer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQProducer producer = new DefaultMQProducer(env.getProperty("app.mq.default-producer-name"));
        // Specify name server addresses.
        producer.setNamesrvAddr(env.getProperty("app.mq.url"));
        //Launch the instance.
        producer.start();
        return producer;
    }

普通消息發送

  1. 生產消息
    /**
     * 發送一個普通消息
     * @param messageInfo 消息內容
     * @return
     */
    public Optional<SendResult> sendNormalEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("創建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String message = JSON.toJSONString(messageInfo.getData());
            Message msg = new Message(topic, tags, message.getBytes(RemotingHelper.DEFAULT_CHARSET));
            //Call send message to deliver message to one of brokers.
            SendResult sendResult = mqProducer.send(msg);
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendNormalEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendNormalEventMessage exception", e);
        }
        LOGGER.info("發送消息, 結果: {}", JSON.toJSONString(result));
        return result;
    }
  1. 消費消息

    /**
     * 創建一個普通消息消費者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "normalMessageConsumer")
    public DefaultMQPushConsumer buildUserMQPullConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

思考:

  1. 一個生產者指定一個topic投放消息, 會在broker端生成多個queue, 消息會被輪詢投放到這些隊列中, 然後消費者訂閱消息, 當隊列數多餘消費者數時, 一個消費者會從多個隊列消費消息;
  2. 原則是一個queue不能被多個消費者消費。那如果不斷有新的消費者上線, queue的個數是否會自動變大, 保持其數目始終大於消費者數呢? 如果queue的個數不變, 那個數超過queue的消費者就沒有可消費的queue了嗎?

發送一組有序消息

基本原理, 是一個queue中的消息是被順序消費的, 所以只要我們將一組有序消息都投放到一個queue中就可以保證這組消息被消息時也是有序的!

  1. 生產消息
    /**
     * 發送一組順序消息
     * @param messageInfo  消息內容
     * @return
     */
    public Optional<SendResult> sendOrderEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("創建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String[] tagsList = tags.split(",");
            String messageStr = JSON.toJSONString(messageInfo.getData());
            SendResult sendResult = null;
            for (int i = 0; i < BATCHSIZE; i++) {
                int orderId = i % 10;
                //Create a message instance, specifying topic, tag and message body.
                Message message = new Message(topic, tagsList[i % tagsList.length], "KEY" + i,
                    (i + messageStr).getBytes(RemotingHelper.DEFAULT_CHARSET));
                sendResult = mqProducer.send(message, (mqs, msg, arg) -> {
                    Integer id = (Integer) arg;
                    int index = id % mqs.size();
                    return mqs.get(index);
                }, orderId);
            }
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendNormalEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendNormalEventMessage exception", e);
        }
        LOGGER.info("發送消息, 結果: {}", JSON.toJSONString(result));
        return result;
    }
  1. 消費消息
    /**
     * 創建一個消費有序消息消費者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "orderMessageConsumer")
    public DefaultMQPushConsumer buildOrderMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "A || C || E");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener(new MessageListenerOrderly() {

            private AtomicLong consumeTimes = new AtomicLong(0);

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(false);
                MessageExt message = msgs.get(0);
                MessageQueue messageQueue = context.getMessageQueue();
                try {
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                this.consumeTimes.incrementAndGet();
                if ((this.consumeTimes.get() % 2) == 0) {
                    return ConsumeOrderlyStatus.SUCCESS;
                } else if ((this.consumeTimes.get() % 3) == 0) {
                    return ConsumeOrderlyStatus.ROLLBACK;
                } else if ((this.consumeTimes.get() % 4) == 0) {
                    return ConsumeOrderlyStatus.COMMIT;
                } else if ((this.consumeTimes.get() % 5) == 0) {
                    context.setSuspendCurrentQueueTimeMillis(3000);
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
                return ConsumeOrderlyStatus.SUCCESS;

            }
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

思考:

  1. 生成環境消費者的上線與下線會進行queue與消費者的重新分配綁定, 那樣也只是說一組有序的消息被從中間隔斷了, 然後這一組消息的前一部分在老的消費者之內被消費保持內部有序, 後續一部分在一個新消費者內部依然有序, 然而這前者和後者的關係是, 前者已經下線, 後者才進行的消息消費, 在客觀時間軸上依然前後有序;

發送一條廣播消息

要實現廣播消息, 在生產者端沒有什麼區別, 依然是發送一條普通的消息即可; 重點在於消費者端, 在默認消費者實現類[DefaultMQPullConsumer, DefaultMQPushConsumer]裏面有個屬性[messageModel], 表示消息是如何被消費者消費;

messageModel 作用
MessageModel.CLUSTERING 消息會被整個consumerGroup中的所有訂閱者分享消費, 一個消息只被某一個消費者消費一次
MessageModel.BROADCASTING 消息會被整個consumerGroup中的所有訂閱者分別消費, 一個消息會被所有訂閱的消費者消費一次
  1. 生產消息

    /**
     * 發送一條廣播消息
     * @param messageInfo  消息內容
     * @return
     */
    public Optional<SendResult> sendBroadcastingEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("創建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String messageStr = JSON.toJSONString(messageInfo.getData());
            SendResult sendResult = null;
            for (int i = 0; i < BATCHSIZE; i++) {
                //Create a message instance, specifying topic, tag and message body.
                Message message = new Message(topic, tags, (i + messageStr).getBytes(RemotingHelper.DEFAULT_CHARSET));
                sendResult = mqProducer.send(message);
            }
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendNormalEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendNormalEventMessage exception", e);
        }
        LOGGER.info("發送消息, 結果: {}", JSON.toJSONString(result));
        return result;
    }

  1. 消費消息
    /**
     * 創建一個消費廣播消息消費者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "broadcastMessageConsumer")
    public DefaultMQPushConsumer buildBroadcastMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "A || C || E");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        //set to broadcast mode
        consumer.setMessageModel(MessageModel.BROADCASTING);

        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

發送一條定時延遲消息

生產者在發送消息時, 指定一個定時延遲消費參數(不同級別代表不同的延遲時段)

DelayTimeLevel 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
延遲時段 不延遲 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 20h
  1. 生產消息

    /**
     * 發送一條定時消息
     * @param messageInfo  消息內容
     * @return
     */
    public Optional<SendResult> sendScheduledEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("創建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String messageStr = JSON.toJSONString(messageInfo.getData());
            SendResult sendResult = null;
            for (int i = 0; i < BATCHSIZE; i++) {
                //Create a message instance, specifying topic, tag and message body.
                Message message = new Message(topic, tags, (i + messageStr).getBytes(RemotingHelper.DEFAULT_CHARSET));
                // This message will be delivered to consumer 1 minutes later.
                message.setDelayTimeLevel(5);
                sendResult = mqProducer.send(message);
            }
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendScheduledEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendScheduledEventMessage exception", e);
        }
        LOGGER.info("發送消息, 結果: {}", JSON.toJSONString(result));
        return result;
    }

  1. 消費消息

    /**
     * 創建一個消費定時消息消費者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "scheduledMessageConsumer")
    public DefaultMQPushConsumer buildScheduledMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

發送一批消息

將消息體積較小的小消息打包批量一次發送, 可以提高系統性能, 條件是這些消息需要具有相同的topic, tag, 同時落地存儲, 並且不支持定時功能, 最後就是整個消息打包後的大小不超過1M;

  1. 生產消息
    /**
     * 發送一批打包消息
     * @param messageInfo  消息內容
     * @return
     */
    public Optional<SendResult> sendBatchEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("創建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String messageStr = JSON.toJSONString(messageInfo.getData());
            List<Message> messages = new ArrayList<>();
            for (int i = 0; i < BATCHSIZE; i++) {
                //Create a message instance, specifying topic, tag and message body.
                messages.add(new Message(topic, tags, i + "key", (i + messageStr).getBytes(RemotingHelper.DEFAULT_CHARSET)));
            }
            SendResult sendResult = mqProducer.send(messages);
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendBatchEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            LOGGER.warn("sendBatchEventMessage exception", e);
        }
        LOGGER.info("發送消息, 結果: {}", JSON.toJSONString(result));
        return result;
    }

  1. 消費消息

    /**
     * 創建一個批量消息消費者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "batchMessageConsumer")
    public DefaultMQPushConsumer buildBatchMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New Message: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

思考: 千萬別以爲一批消息打包了, 到消費者端就是在一個線程中被按打包的順序依次消費處理, 雖然打包的消息肯定是被髮送到一個消費者上的,但是包會被拆解, 交由消費者中多個線程併發處理, 所以別想通過打包實現消息的順序消費, 哈哈

發送事務消息

RocketMQ支持事物消息: 在分佈式環境下通過兩階段提交實現最終一致性; 事務性消息可以確保以原子方式執行本地事務並進行消息的發送;

事務消息的使用約束

  1. 不支持定時和批量發送;
  2. 爲了避免一個消息被檢查多次而導致其他部分消息累積得不到處理, 我們默認限制一個消息最多被檢查15次, 但是用戶可以通過參數 “transactionCheckMax” 修改此默認值; 如果一條消息被檢查超過最大檢查數, 那broker就會丟棄此消息並默認同時輸出錯誤日誌, 用戶也可以通過 override “AbstractTransactionCheckListener” class 改變此默認行爲;
  3. Broker中配置的參數“transactionTimeout”可以決定一個事務消息會被檢查多久, 然後再發送消息時用戶可以通過設置user property “CHECK_IMMUNITY_TIME_IN_SECONDS”改變此全局配置值;
  4. 一個事務消息可以被檢查和消費多次;
  5. 事務消息在做Committed時可能會投放到用戶目標topic失敗, 高可用是由RocketMQ自身的機制保證的; 如果你想確保事務消息不丟失並且保證完整性, 那就需要使用雙重同步刷盤;
  6. 消息事務的生產者ID不能和其他類型消息的生產者ID共享, 因爲事務消息允許逆向查詢;

代碼實現

  1. 創建一個事務消息狀態檢查器, 構建一個事務消息生產者

/**
 * @author Hinsteny
 * @version TransactionListenerImpl: TransactionListenerImpl 2019-05-16 16:44 All rights reserved.$
 */
public class TransactionListenerImpl implements TransactionListener {

    private final Logger logger = LogManager.getLogger(this.getClass());

    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:{
                    logger.info("COMMIT_MESSAGE message: {}", msg.getMsgId());
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        }
        logger.info("COMMIT_MESSAGE message: {}", msg.getMsgId());
        return LocalTransactionState.COMMIT_MESSAGE;
    }

}

    @Bean(name = "transactionMQProducer")
    public DefaultMQProducer buildTransactionMQProducer() throws MQClientException {
        //Instantiate with a producer group name.
        TransactionMQProducer  producer = new TransactionMQProducer(env.getProperty("app.mq.default-producer-name"));
        // Specify name server addresses.
        producer.setNamesrvAddr(env.getProperty("app.mq.url"));
        TransactionListener transactionListener = new TransactionListenerImpl();
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> {
            Thread thread = new Thread(r);
            thread.setName("client-transaction-msg-check-thread");
            return thread;
        });
        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        //Launch the instance.
        producer.start();
        return producer;
    }

  1. 發送事務消息


    /**
     * 發送事務消息
     * @param messageInfo  消息內容
     * @return
     */
    public Optional<SendResult> sendTransactionEventMessage(EventMessageInfo messageInfo) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("創建MQ消息: {}", JSON.toJSONString(messageInfo));
        }
        Optional<SendResult> result = Optional.empty();
        try {
            validateMessageInfo(messageInfo);
            String topic = StringUtils.isNoneBlank(messageInfo.getTopic()) ? messageInfo.getTopic() : DEFAULT_TOPIC;
            String tags = StringUtils.isNoneBlank(messageInfo.getTags()) ? messageInfo.getTags() : DEFAULT_TAGS;
            String messageStr = JSON.toJSONString(messageInfo.getData());
            Message messages = new Message(topic, tags, messageStr.getBytes(RemotingHelper.DEFAULT_CHARSET));
            SendResult sendResult = mqProducer.sendMessageInTransaction(messages, null);
            result = Optional.of(sendResult);
        } catch (IllegalArgumentException e) {
            LOGGER.warn("sendTransactionEventMessage IllegalArgumentException", e);
        } catch (UnsupportedEncodingException | MQClientException e) {
            LOGGER.warn("sendTransactionEventMessage exception", e);
        }
        LOGGER.info("發送消息, 結果: {}", JSON.toJSONString(result));
        return result;
    }

  1. 消費消息

    /**
     * 創建一個事務消息消費者
     * @return
     * @throws MQClientException
     */
    @Bean(name = "transactionMessageConsumer")
    public DefaultMQPushConsumer buildTransactionMQPushConsumer() throws MQClientException {
        //Instantiate with a producer group name.
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(env.getProperty("app.mq.default-consumer-name"));
        // Specify name server addresses.
        consumer.setNamesrvAddr(env.getProperty("app.mq.url"));
        // Subscribe one more more topics to consume.
        consumer.subscribe(env.getProperty("app.message.user.topic"), "*");
        // Register callback to execute on arrival of messages fetched from brokers.
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            msgs.stream().forEach(message -> {
                try {
                    MessageQueue messageQueue = context.getMessageQueue();
                    logger.info("From brokerName [{}], QueueId [{}], Topic[{}], Tags[{}], Keys[{}], Receive New MessageID: {}", messageQueue.getBrokerName(), messageQueue.getQueueId(),
                        message.getTopic(), message.getTags(), message.getKeys(), message.getMsgId());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Message content: {}",  new String(message.getBody(), RemotingHelper.DEFAULT_CHARSET));
                    }
                } catch (UnsupportedEncodingException e) {
                    logger.warn("exception", e);
                }
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        //Launch the consumer instance.
        consumer.start();
        return consumer;
    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章