Spring Boot整合RocketMQ之事務消息

上次簡單的了解了一下在Spring Boot下通過使用rocketmq-spring-boot-starter進行普通消息的發送、接收以及使用集群模式來模擬實現廣播模式,文章鏈接。今天來學習一下RocketMQ事務消息的發送。
RocketMQ的事務消息分為3種狀態,分別是提交狀態、回滾狀態、中間狀態:

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

當然因為在項目中我使用的是rocketmq-spring-boot-starter,所以表述上略有不同,但是本質是一樣的。
事務消息在解決分布式事務的場景中感覺還是很有用的,雖然我們現在項目的分布式事務是通過Seata來實現的,但是通過事務消息或者消息的最終一次性也是可以的。
事務消息總共分為3個階段:發送Prepared消息、執行本地事務、發送確認消息。這三個階段是前后關聯的,只有發送Prepared消息成功,才會執行本地事務,本地事務返回的狀態是提交,那么就會發送最終的確認消息。如果在結束消息事務時,本地事務狀態失敗,那么Broker回查線程定時(默認1分鐘)掃描每個存儲事務狀態的表格文件,如果是已經提交或者回滾的消息直接跳過,如果是Prepared狀態則會向生產者發起一個檢查本地事務的請求。

一、代碼修改

首先我創建有一個Service來發送事務消息,代碼沒有什么特殊的含義,只是拿來當一個demo,代碼如下:

    public Boolean save(OrderEntity orderEntity) {
        Message<OrderEntity> message = MessageBuilder.withPayload(orderEntity).build();

        log.info(">>>> send tx message start,tx_group={},destination={},payload={} <<<<",TX_GROUP,ORDER_TOPIC + ORDER_TAG,orderEntity);
        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction("tx_order","order_topic:" + "tx_tag",message,orderEntity.getUserName());
        String sendStatus = sendResult.getSendStatus().name();
        String localTXState = sendResult.getLocalTransactionState().name();
        log.info(">>>> send status={},localTransactionState={} <<<<",sendStatus,localTXState);
        return Boolean.TRUE;
    }

使用RocketMQTemplate發送事務消息和普通消息略有不同的是,需要指一個事務生產者組,當然如果傳入null,則會使用默認值rocketmq_transaction_default_global_name,發生消息的地址和普通消息一樣都Topic:Tag,另外一點不同的是除了發生的Message之外,還可以發送其他的額外參數,不過這些參數只會在執行本地事務的時候會用到。
接下來我們創建一個消息的監聽器(消費者),這個和普通消息的監聽器一樣,代碼如下:

@Component
@RocketMQMessageListener(consumerGroup = "tx_consumer",topic = "order_topic")
public class OrderListener implements RocketMQListener<String>{

    @Override
    public void onMessage(String message) {
        log.info(">>>> message={} <<<<",message);
    }
}

除了消費者之外,我們還需要創建事務消息生產者端的消息監聽器,注意是生產者,不是消費者,我們需要實現的是RocketMQLocalTransactionListener接口,代碼如下:

@RocketMQTransactionListener(txProducerGroup = "tx_order")
public class OrderTXMsgListener implements RocketMQLocalTransactionListener {

    @Autowired
    private UserRepository userRepository;

    private static final Gson GSON = new Gson();

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        log.info(">>>> TX message listener execute local transaction, message={},args={} <<<<",msg,arg);
        // 執行本地事務
        RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
        try {
            String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
            OrderEntity orderEntity = GSON.fromJson(jsonString, OrderEntity.class);
            String userName = (String) arg;
        } catch (Exception e) {
            log.error(">>>> exception message={} <<<<",e.getMessage());
            result = RocketMQLocalTransactionState.UNKNOWN;
        }
        return result;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        log.info(">>>> TX message listener check local transaction, message={} <<<<",msg.getPayload());
        // 檢查本地事務
        RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
        try {
            String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
            OrderEntity orderEntity = GSON.fromJson(jsonString, OrderEntity.class);
        } catch (Exception e) {
            // 異常就回滾
            log.error(">>>> exception message={} <<<<",e.getMessage());
            result = RocketMQLocalTransactionState.ROLLBACK;
        }
        return result;
    }
}

@RocketMQTransactionListener表明這個一個生產端的消息監聽器,需要配置監聽的事務消息生產者組。而實現RocketMQLocalTransactionListener接口,重寫執行本地事務的方法和檢查本地事務方法。下面,我們通過修改生產者端事務監聽器的代碼來觀察代碼的執行情況。

二、消息事務測試

首先還是正常的啟動項目,在執行本地事務方法中正常情況下返回的值是COMMIT,即提交事務,這種情況下消費者會直接消費消息,而略過檢查本地事務的方法。調用該接口,項目日志輸出如下:

>>>> send tx message start,tx_group=tx_order,destination=order_topic:tx_tag,payload=OrderEntity(id=null, userName=lisi, price=8848.00, address=CN-SC-CD-05, createTime=null, updateTime=null, status=20) <<<<
>>>> TX message listener execute local transaction, message=GenericMessage [payload=byte[119], headers={rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_TRANSACTION_ID=C0A800690C3418B4AAC2842438960000, rocketmq_TAGS=tx_tag, id=f32f4848-9acf-20bb-2501-0e6088765897, contentType=application/json, timestamp=1595749766307}],args=lisi <<<<
>>>> send status=SEND_OK,localTransactionState=COMMIT_MESSAGE <<<<
>>>> message={"id":null,"userName":"lisi","price":8848.00,"address":"CN-SC-CD-05","createTime":null,"updateTime":null,"status":"20"} <<<<

通過日志分析可以看出,在執行完本地事務方法之后,返回的本地事務狀態是COMMIT_MESSAGE,接著消費者消費消息,和我們的預期是一樣的。
接下來我們修改下執行本地事務的方法,讓該方法返回狀態為RocketMQLocalTransactionState.UNKNOWN,修改之后如下:

    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        log.info(">>>> TX message listener execute local transaction, message={},args={} <<<<",msg,arg);
        // 執行本地事務
        RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
        try {
            String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
            OrderEntity orderEntity = GSON.fromJson(jsonString, OrderEntity.class);
            String userName = (String) arg;
            int r = 11 / 0;
        } catch (Exception e) {
            log.error(">>>> exception message={} <<<<",e.getMessage());
            result = RocketMQLocalTransactionState.UNKNOWN;
        }
        return result;
    }

這樣因為發生異常,該方法返回的結果是UNKNOWN,根據上文的分析,執行本地事務方法之后應該會執行檢查本地事務方法,重啟項目之后,再次調用一下接口,查看日志輸出如下:

>>>> send tx message start,tx_group=tx_order,destination=order_topic:tx_tag,payload=OrderEntity(id=null, userName=zhangsan, price=90001.00, address=CN-SC-CD-02, createTime=null, updateTime=null, status=10) <<<<
>>>> TX message listener execute local transaction, message=GenericMessage [payload=byte[124], headers={rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC2842BF39A0000, rocketmq_TAGS=tx_tag, id=dfd215f4-2aa6-f377-d1a7-ebbe3875769a, contentType=application/json, timestamp=1595750272928}],args=zhangsan <<<<
>>>> exception message=/ by zero <<<<
>>>> send status=SEND_OK,localTransactionState=UNKNOW <<<<
HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m22s578ms430μs170ns).
>>>> TX message listener check local transaction, message=GenericMessage [payload=byte[124], headers={rocketmq_QUEUE_ID=0, TRANSACTION_CHECK_TIMES=1, rocketmq_TAGS=tx_tag, rocketmq_BORN_TIMESTAMP=1595750272923, rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_MESSAGE_ID=C0A8006900002A9F00000000000156AB, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC2842BF39A0000, rocketmq_SYS_FLAG=0, id=ea3c3a7a-23c6-5acf-4c0f-0fa42f795b41, rocketmq_BORN_HOST=192.168.0.105, contentType=application/json, timestamp=1595750310890}] <<<<
>>>> TX message listener check local transaction, message=GenericMessage [payload=byte[124], headers={rocketmq_QUEUE_ID=0, TRANSACTION_CHECK_TIMES=2, rocketmq_TAGS=tx_tag, rocketmq_BORN_TIMESTAMP=1595750272923, rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_MESSAGE_ID=C0A8006900002A9F0000000000015892, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC2842BF39A0000, rocketmq_SYS_FLAG=0, id=cddfa35c-c8b2-cb1b-dce7-a26c6888b99a, rocketmq_BORN_HOST=192.168.0.105, contentType=application/json, timestamp=1595750374536}] <<<<
>>>> message={"id":null,"userName":"zhangsan","price":90001.00,"address":"CN-SC-CD-02","createTime":null,"updateTime":null,"status":"10"} <<<<
>>>> message={"id":null,"userName":"zhangsan","price":90001.00,"address":"CN-SC-CD-02","createTime":null,"updateTime":null,"status":"10"} <<<<

根據日志輸出,在Service中返回的事務消息發送狀態是SEND_OK,但是返回的本地事務狀態是UNKNOW,所以需要執行檢查本地事務方法,但是這里出現了一個問題就是檢查本地事務方法執行了兩次,而且事務消息也被消費了兩次,感覺有點不正常了,但是檢查發現兩條信息日志中rocketmq_TRANSACTION_ID是一樣的,這是什么情況??會不會和HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m22s578ms430μs170ns).有關呢,因為當時自己使用的DEBUG模式,看代碼停留了一段時間,這樣導致Broker發起的第一個回查線程掛起,而這時Broker又啟動了一個線程,從而執行了兩次檢查事務的代碼,而該方法返回的是COMMIT,所以。
不使用DEBUG模式重新測試一下,日志如下:

>>>> send tx message start,tx_group=tx_order,destination=order_topic:tx_tag,payload=OrderEntity(id=null, userName=wangwu, price=9876.00, address=CN-SC-CD-00, createTime=null, updateTime=null, status=10) <<<<
>>>> TX message listener execute local transaction, message=GenericMessage [payload=byte[121], headers={rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC28432E4130005, rocketmq_TAGS=tx_tag, id=464edcfe-09c1-cc4a-5ac3-f3df888b0102, contentType=application/json, timestamp=1595750727701}],args=wangwu <<<<
>>>> exception message=/ by zero <<<<
>>>> send status=SEND_OK,localTransactionState=UNKNOW <<<<
>>>> TX message listener check local transaction, message=GenericMessage [payload=byte[121], headers={rocketmq_QUEUE_ID=3, TRANSACTION_CHECK_TIMES=1, rocketmq_TAGS=tx_tag, rocketmq_BORN_TIMESTAMP=1595750727699, rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_MESSAGE_ID=C0A8006900002A9F0000000000016109, rocketmq_TRANSACTION_ID=C0A800690E9D18B4AAC28432E4130005, rocketmq_SYS_FLAG=0, id=77765356-fc4d-6d05-3531-6a67fbbed7f7, rocketmq_BORN_HOST=192.168.0.105, contentType=application/json, timestamp=1595750790917}] <<<<
>>>> message={"id":null,"userName":"wangwu","price":9876.00,"address":"CN-SC-CD-00","createTime":null,"updateTime":null,"status":"10"} <<<<

這里輸出的日志信息又沒有問題了,我個人認為上面應該就是DEBUG導致的,這里就不再探討了。
接下來測試一下,在執行本地事務方法中返回ROLLBACK的情況,這里代碼就省略了,直接返回ROLLBACK。日志輸出如下:

>>>> send tx message start,tx_group=tx_order,destination=order_topic:tx_tag,payload=OrderEntity(id=null, userName=zhaoliu, price=10000.00, address=CN-SC-CD-03, createTime=null, updateTime=null, status=10) <<<<
>>>> TX message listener execute local transaction, message=GenericMessage [payload=byte[123], headers={rocketmq_TOPIC=order_topic, rocketmq_FLAG=0, rocketmq_TRANSACTION_ID=C0A800691A3A18B4AAC284F72B910000, rocketmq_TAGS=tx_tag, id=d5b24a82-8d8b-90ad-7322-adfe2c4f3026, contentType=application/json, timestamp=1595763591062}],args=zhaoliu <<<<
>>>> exception message=/ by zero <<<<
>>>> send status=SEND_OK,localTransactionState=ROLLBACK_MESSAGE <<<<

沒有執行檢驗本地事務的方法,和之前說的一樣。到這里我覺得應該基本上可以明白生產者端消息監聽器中兩個方法的具體作用了,主要還是理解RocketMQ事務消息的基本原理。
校驗本地事務方法的返回值和執行本地事務方法的返回值的作用是一樣的,這里就不再測試了。
網上找了一個圖,感覺非常的直觀:

圖-1.png

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。