RocketMQ進擊(六)磕一磕RocketMQ的事務消息


楔子:有句老話已經在電視裏面聽到,我們不成功便成仁。最終是要完成任務。

 

1. 不成功便成仁

RocketMQ 事務消息(Transactional Message)是指應用本地事務和發送消息操作可以被定義到全局事務中,要麼同時成功,要麼同時失敗。通過事務消息達到分佈式事務的最終一致。

Apache RocketMQ 在 4.3.0 版中已經支持分佈式事務消息,它採用了 2PC 的思想來實現了提交事務消息,同時增加一個補償邏輯來處理二階段超時或者失敗的消息。如下圖所示:

上圖說明了事務消息的大致方案,其中可以分爲兩個流程:正常事務消息的發送及提交、事務消息的補償流程。

一】事務消息發送及提交:

1)Producer 向 Broker 發送消息(half 消息)
2)服務端響應消息寫入結果
3)Producer 根據發送結果執行本地事務(如果寫入失敗,此時 half 消息對業務不可見,本地邏輯不執行)
4)Producer 根據本地事務狀態執行 Commit 或者 Rollback(Commit 操作生成消息索引,消息對消費者可見)

二】補償流程:

5)對沒有 Commit/Rollback 的事務消息(pending 狀態的消息),從服務端發起一次“回查”
6)Producer 收到回查消息,檢查回查消息對應的本地事務的狀態
7)根據本地事務狀態,重新 Commit 或者 Rollback

其中,補償階段用於解決消息 Commit 或者 Rollback 發生超時或者異常失敗的情況。

 

2. 閒時二三閒事

2.1. 源碼定義

2.1.1 事務的三種狀態

事務消息共有三種狀態,提交狀態、回滾狀態、中間狀態:

  • TransactionStatus.CommitTransaction:提交事務,它允許消費者消費此消息。
  • TransactionStatus.RollbackTransaction:回滾事務,它代表該消息將被刪除,不允許被消費。
  • TransactionStatus.Unknown:中間狀態,它代表需要檢查消息隊列來確定狀態。
package org.apache.rocketmq.client.producer;

public enum LocalTransactionState {
    COMMIT_MESSAGE,
    ROLLBACK_MESSAGE,
    UNKNOW;

    private LocalTransactionState() {
    }
}

2.2. 代碼示例

這裏通過一個常用場景簡單模擬 RocketMQ 的事務消息:寫2個微服務,分別是訂單服務和商品服務。訂單服務進行下單處理,併發送消息給商品服務,商品服務對下單成功的商品進行扣減庫存。

2.2.1 創建事務性生產者

首先,寫一個簡易訂單服務,使用 TransactionMQProducer 類創建生產者,並指定唯一的 ProducerGroup,就可以設置自定義線程池來處理這些檢查請求。執行本地事務後、需要根據執行結果對消息隊列進行狀態回傳。回傳的三種事務狀態如上所述。

package com.meiwei.service.mq.tcp.producer;

import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 訂單服務
 */
public class OrderService {

    // Topic 爲 Message 所屬的一級分類,就像學校裏面的初中、高中
    // Topic 名稱長度不得超過 64 字符長度限制,否則會導致無法發送或者訂閱
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";
    // Tag 爲 Message 所屬的二級分類,比如初中可分爲初一、初二、初三;高中可分爲高一、高二、高三
    private static final String MQ_CONFIG_TAG_PUSH = "PID_MEIWEI_SMS_ORDER_TRANSACTION";

    public static void main(String[] args) throws Exception {
        TransactionMQProducer producer = new TransactionMQProducer();

        // 指定 NameServer 地址列表,多個nameServer地址用半角分號隔開。此處應改爲實際 NameServer 地址
        // NameServer 的地址必須有,但也可以通過啓動參數指定、環境變量指定的方式設置,不一定要寫死在代碼裏
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.setProducerGroup("meiwei-producer-transaction-mq");

        // 自定義線程池,執行事務操作
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 10L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(20), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("meiwei-order-service-transaction-msg-check");
                return thread;
            }
        });
        producer.setExecutorService(executor);

        // 設置事務消息監聽器
        producer.setTransactionListener(new OrderTransactionListener());

        // 在發送MQ消息前,必須調用 start 方法來啓動 Producer,只需調用一次即可
        producer.start();
        System.out.println("Order Server Start.");

        // 模擬業務
        for (int i = 0; i < 5; i++) {
            String orderId = System.currentTimeMillis() + "";
            String payOrder = "下單完成,訂單編號:" + orderId;
            Message message = new Message(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_PUSH, orderId, payOrder.getBytes(RemotingHelper.DEFAULT_CHARSET));

            // 發送事務消息
            TransactionSendResult result = producer.sendMessageInTransaction(message, orderId);
            System.out.println("【發送事務消息】發送結果:" + result);

            Thread.sleep(100);
        }
    }
}

2.2.2 實現事務的監聽接口

事務消息需要註冊一個 TransactionListener,進行本地事務的執行和事務回查,代碼如下:

package com.meiwei.service.mq.tcp.producer;

import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 訂單事務消息監聽器
 */
public class OrderTransactionListener implements TransactionListener {

    private static final Map<String, Integer> statusMap = new ConcurrentHashMap<>();

    // 執行本地事務
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        String orderId = (String) o;

        // 記錄本地事務執行結果
        Integer status = this.executeTransactionResult(orderId);
        System.out.println("【訂單事務消息監聽器】本地有新訂單,orderId: " + orderId + ", result: " + status);

        // 返回中間狀態,需要檢查消息隊列來確定狀態,即觸發 checkLocalTransaction
        return LocalTransactionState.UNKNOW;
    }

    // 檢查本地事務狀態,並回應消息隊列的檢查請求
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        Integer status = statusMap.get(messageExt.getKeys());
        System.out.println("【訂單事務消息監聽器】執行事務消息回查,orderId: " + messageExt.getKeys() + ", result: " + status + ",時間: " + new Date());

        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;
    }

    // 模擬一個業務場景,並返回訂單處理狀態 
    private Integer executeTransactionResult(String orderId) {
        Integer status = Math.toIntExact(Long.valueOf(orderId) % 3);
        statusMap.put(orderId, status);
        return status;
    }
}

2.2.3 創建事務性消費者

再次,寫一個簡易的商品服務,接收訂單服務的事務消息,如果消息成功 commit,則進行本地扣減庫存。

package com.meiwei.service.mq.tcp.consumer;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;

/**
 * 商品服務
 */
public class DrugsService {

    // Topic 爲 Message 所屬的一級分類,就像學校裏面的初中、高中
    // Topic 名稱長度不得超過 64 字符長度限制,否則會導致無法發送或者訂閱
    private static final String MQ_CONFIG_TOPIC = "TOPIC_MEIWEI_SMS_NOTICE_TEST";
    // Tag 爲 Message 所屬的二級分類,比如初中可分爲初一、初二、初三;高中可分爲高一、高二、高三
    private static final String MQ_CONFIG_TAG_PUSH = "PID_MEIWEI_SMS_ORDER_TRANSACTION";

    public static void main(String[] args) throws Exception {
        // 聲明並初始化一個 consumer
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
        // 指定 NameServer 地址列表,多個nameServer地址用半角分號隔開。此處應改爲實際 NameServer 地址
        // NameServer 的地址必須有,但也可以通過啓動參數指定、環境變量指定的方式設置,不一定要寫死在代碼裏
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.setConsumerGroup("meiwei-consumer-transaction-mq");
        // 設置 consumer 所訂閱的 Topic 和 Tag,這裏的 Topic 需要與生產者保持一致
        consumer.subscribe(MQ_CONFIG_TOPIC, MQ_CONFIG_TAG_PUSH);
        // 註冊一個消息監聽器消費消息
        consumer.registerMessageListener(new DrugsMqConsumerListener());

        consumer.start();
        System.out.println("Drugs Service Start.");
    }
}

2.2.4 實現消息消費監聽器

package com.meiwei.service.mq.tcp.consumer;

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.Collections;
import java.util.List;
import java.util.Optional;

/**
 * 商品消費端監聽器
 */
public class DrugsMqConsumerListener implements MessageListenerConcurrently {

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        Optional.ofNullable(list).orElse(Collections.emptyList()).forEach(msg -> {
            String orderId = msg.getKeys();
            System.out.println("【商品消費端監聽器】您有新訂單,orderId: " + orderId + ",商品庫存需要更新");
        });

        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
}

 

2.3. 測試與結果

訂單服務作爲生產者發出新訂單扣減庫存消息:

Order Server Start.
【訂單事務消息監聽器】本地有新訂單,orderId: 1571021207194, result: 1
【發送事務消息】發送結果:SendResult [sendStatus=SEND_OK, msgId=0A06341B156C18B4AAC24542D3BF0000, offsetMsgId=null, messageQueue=MessageQueue [topic=TOPIC_MEIWEI_SMS_NOTICE_TEST, brokerName=YYW-SH-PC-1454, queueId=1], queueOffset=1620]
【訂單事務消息監聽器】本地有新訂單,orderId: 1571021207595, result: 0
【發送事務消息】發送結果:SendResult [sendStatus=SEND_OK, msgId=0A06341B156C18B4AAC24542D42B0001, offsetMsgId=null, messageQueue=MessageQueue [topic=TOPIC_MEIWEI_SMS_NOTICE_TEST, brokerName=YYW-SH-PC-1454, queueId=2], queueOffset=1621]
【訂單事務消息監聽器】本地有新訂單,orderId: 1571021207696, result: 2
【發送事務消息】發送結果:SendResult [sendStatus=SEND_OK, msgId=0A06341B156C18B4AAC24542D4900002, offsetMsgId=null, messageQueue=MessageQueue [topic=TOPIC_MEIWEI_SMS_NOTICE_TEST, brokerName=YYW-SH-PC-1454, queueId=3], queueOffset=1622]
【訂單事務消息監聽器】本地有新訂單,orderId: 1571021207798, result: 2
【發送事務消息】發送結果:SendResult [sendStatus=SEND_OK, msgId=0A06341B156C18B4AAC24542D4F60003, offsetMsgId=null, messageQueue=MessageQueue [topic=TOPIC_MEIWEI_SMS_NOTICE_TEST, brokerName=YYW-SH-PC-1454, queueId=0], queueOffset=1623]
【訂單事務消息監聽器】本地有新訂單,orderId: 1571021207900, result: 2
【發送事務消息】發送結果:SendResult [sendStatus=SEND_OK, msgId=0A06341B156C18B4AAC24542D55C0004, offsetMsgId=null, messageQueue=MessageQueue [topic=TOPIC_MEIWEI_SMS_NOTICE_TEST, brokerName=YYW-SH-PC-1454, queueId=1], queueOffset=1624]
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207194, result: 1,時間: Mon Oct 14 10:46:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:46:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207798, result: 2,時間: Mon Oct 14 10:46:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207696, result: 2,時間: Mon Oct 14 10:46:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207900, result: 2,時間: Mon Oct 14 10:46:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:47:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:48:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:49:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:50:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:51:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:52:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:53:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:54:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:55:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:56:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:57:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:58:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 10:59:56 CST 2019
【訂單事務消息監聽器】執行事務消息回查,orderId: 1571021207595, result: 0,時間: Mon Oct 14 11:00:56 CST 2019

商品服務作爲消費者監聽並消費到扣減庫存的消息,後面就可以進行扣減庫存的業務處理:

從上面的生產者輸出日誌可以看到訂單服務發出的 5 條消息:

  • 訂單狀態爲 UNKNOW:0 的有 1 條,這條消息被回查了 15 次(transactionCheckMax 默認值 15,可配置)
  • 訂單狀態爲 COMMIT_MESSAGE:1 的有 1 條,且被商品服務正常消費
  • 訂單狀態爲 ROLLBACK_MESSAGE:2 的有 3 條,且未被商品服務消費,也未被回查

 

2.4. 使用限制

  1. 事務消息不支持延時消息和批量消息。
  2. 事務性消息可能不止一次被檢查或消費。
  3. 爲了避免單個消息被檢查太多次而導致半隊列消息累積,源碼默認將單個消息的檢查次數限制爲 15 次,但是我們可以通過 Broker 配置文件的 transactionCheckMax 參數來修改這個限制。如果已經檢查某條消息超過 N 次的話(N =  transactionCheckMax),Broker 則將丟棄此消息,並在默認情況下同時打印錯誤日誌。不過可以通過重寫  AbstractTransactionCheckListener 類來修改這個行爲。
  4. 事務消息 將在 Broker 配置文件中的參數 transactionMsgTimeout 這樣的特定時間長度之後 被檢查。不過可以通過設置屬性 CHECK_IMMUNITY_TIME_IN_SECONDS 來改變這個限制,該參數優先於 transactionMsgTimeout 參數。
  5. 提交給用戶的目標主題消息可能會失敗,目前這依日誌的記錄而定。它的高可用性通過 RocketMQ 本身的高可用性機制來保證,如果希望確保事務消息不丟失、並且事務完整性得到保證,建議使用同步的雙重寫入機制。
  6. 事務消息的生產者 ID 不能與其他類型消息的生產者 ID 共享。與其他類型的消息不同,事務消息允許反向查詢、MQ服務器能通過它們的生產者 ID 查詢到消費者。

 


RocketMQ進擊物語:
RocketMQ進擊(零)RocketMQ這個大水池子
RocketMQ進擊(一)Windows環境下安裝部署Apache RocketMQ
RocketMQ進擊(二)一個默認生產者,兩種消費方式,三類普通消息詳解分析
RocketMQ進擊(三)順序消息與高速公路收費站
RocketMQ進擊(四)定時消息(延時隊列)
RocketMQ進擊(五)集羣消費模式與廣播消費模式
RocketMQ進擊(六)磕一磕RocketMQ的事務消息
RocketMQ進擊(七)盤一盤RocketMQ的重試機制
RocketMQ進擊(八)RocketMQ的日誌收集Logappender
RocketMQ異常:RocketMQ順序消息收不到或者只能收到一部分消息
RocketMQ異常:Unrecognized VM option 'MetaspaceSize=128m'

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