Implementation of rocketmq distributed transaction

Posted by Blockis on Fri, 11 Feb 2022 12:45:16 +0100

1. Transaction scenario

Scenario: ICBC user A transfers 10000 yuan to CCB user B;

Steps:

  • 1. ICBC system sends a synchronization message M to Broker, which increases the payment of RMB 10000 to B
  • 2. After the message is successfully received by the Broker, send a successful ACK to the ICBC system
  • 3. ICBC system will deduct 10000 yuan from user A after receiving the successful ACK
  • 4. CCB system obtains message M from Broker
  • 5. CCB system consumption message M, that is, add 10000 yuan to user B

There is a problem: if the deduction operation in step 3 fails, but the message has been successfully sent to the Broker. For MQ, as long as the message is written successfully, the message can be consumed; At this time, user B in the CCB system will increase by 10000 yuan, resulting in data inconsistency.

2. Solution ideas

Solution: make steps 1, 2 and 3 of the above steps atomic, either all successful or all failed. That is, after the message is sent successfully, the deduction must be successful. If the deduction fails, the message of successful sending will be rolled back. In short, a message will be submitted in advance. When there is no abnormality in the local transaction, a confirmation message will be sent again; The idea is to use transaction messages. Here, we need to use distributed transaction solutions;

Use the transaction message to process the requirement scenario:

  1. The transaction manager TM sends an instruction to the transaction coordinator TC to start the global transaction

  2. The ICBC system sends a transaction message M to TC for an increase of RMB 10000 to B

  3. TC will send a semi transaction message prepareHalf to the Broker and pre submit the message M to the Broker. At this time, the CCB system cannot see the message M in the Broker

  4. The Broker will Report the pre submitted execution results to TC.

  5. If the pre submission fails, TC will report the response of pre submission failure to TM, and the global transaction ends; If the pre submission is successful, TC will call the callback operation of ICBC system to complete the operation of withholding 10000 yuan from ICBC user A

  6. ICBC system will send the withholding execution result to TC, that is, the execution status of local transactions

  7. After receiving the withholding execution result, TC will report the result to TM

  8. TM will send different confirmation instructions to TC according to the report results

    • If the withholding is successful (the local transaction status is COMMIT_MESSAGE), TM sends a Global Commit instruction to TC
    • If the withholding fails (the local transaction status is ROLLBACK_MESSAGE), TM sends a Global Rollback instruction to TC
    • If the status is unknown (the local transaction status is unknown), the local transaction status check operation of ICBC system will be triggered. The backcheck operation will the backcheck result, i.e. COMMIT_MESSAGE or ROLLBACK_MESSAGE Report to TC. TC reports the results to TM, and TM will send the final confirmation instruction Global Commit or Global Rollback to TC
  9. After receiving the instruction, TC will send confirmation instruction to Broker and ICBC system

    • If the TC receives A Global Commit instruction, it will send A Branch Commit instruction to the Broker and ICBC system. At this time, the message M in the Broker can be seen by the CCB system; At this time, the deduction operation in ICBC user A is really confirmed;
    • If the TC receives A Global Rollback instruction, it will send A Branch Rollback instruction to the Broker and ICBC system. At this time, the message M in the Broker will be revoked; The deduction operation in ICBC user A will be rolled back;

3. Transaction message concept

The transaction message of RocketMQ is mainly through the asynchronous processing of messages, which can ensure the successful execution or failure of local transactions and message sending at the same time, so as to ensure the final consistency of data. The specific process is as follows:

Transaction messages have three states: commit state, rollback state and intermediate state:

  • RocketMQLocalTransactionState.COMMIT: commit a transaction that allows the consumer to consume this message.
  • RocketMQLocalTransactionState.ROLLBACK: rollback transaction, which means that the message will be deleted and cannot be consumed.
  • RocketMQLocalTransactionState.UNKNOWN: intermediate status, which means that the message queue needs to be checked to determine the status.

Transaction process:
Send the message to the broker through sendMessageInTransaction method and call back the transaction listener method. At this time, the message is in semi message state and needs to be confirmed twice before it can be sent to the queue and consumed by the consumer.

If the transaction status received by MQ is always UNKNOWN, it will continue to send a callback to the MQ sender to check the local transaction status until the message of Commit/Rollback status is received or the message of UNKNOWN status is deleted by manual intervention;

Note that local SQL transactions must be executed in the callback method of MQ listener

4. Transaction message implementation

4.1 code Description:

The core classes are TxConsumer, TxProducer and TxProducerListener to implement the process. TxProducer calls the sendMessageInTransaction method and enters the TxProducer listener

Note that local SQL transactions must be executed in the callback method of MQ listener

It should be noted that a RocketMQTemplate can only register one transaction listener. If there are multiple transaction listeners listening to different producers, you need to define different rocketmqtemplates through the annotation @ ExtRocketMQTemplateConfiguration, for example:

  // Define RocketMQTemplate
  @ExtRocketMQTemplateConfiguration
  public class XXXRocketMQTemplate extends RocketMQTemplate {
  
  }
  
  // Consumer sends transaction message
  // Import custom RocketMQTemplate
  @Resource(name = "xxxRocketMQTemplate")
  RocketMQTemplate rocketMQTemplate;
  
  public void send() {
    Message<String> message = MessageBuilder.withPayload("text").build();
    rocketMQTemplate.sendMessageInTransaction("topic", message, null);
  }

To use transaction messages, you need to customize the message listener and bind it with RocketMQTemplate:

// Listener. The default is rocketMQTemplate
// If ExtRocketMQTemplateConfiguration is a custom RocketMQTemplate, you need to bind the custom bean name
@RocketMQTransactionListener(rocketMQTemplateBeanName="txRocketMQTemplate")
public class TxProducerListener implements RocketMQLocalTransactionListener {
   
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        .....
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        .....
    }
}

4.2 complete code

TxConsumer:

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "tx_topic", consumerGroup = "tx_group")
@Slf4j
public class TxConsumer implements RocketMQListener<String> {

    /**
     *
     * @param message
     */
    @Override
    public void onMessage(String message) {
        log.info("Message transaction-Message received:" + message);
    }
}

TxRocketMQTemplate:

// A RocketMQTemplate can only register one transaction listener. If there are multiple transaction listeners listening to different ` producers'`
// Different rocketmqtemplates need to be defined through the annotation ` @ ExtRocketMQTemplateConfiguration '
@ExtRocketMQTemplateConfiguration
public class TxRocketMQTemplate extends RocketMQTemplate {

}

TxProducerListener:

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;

import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@RocketMQTransactionListener(rocketMQTemplateBeanName = "txRocketMQTemplate")
public class TxProducerListener implements RocketMQLocalTransactionListener {

    /**
     * Record the status of each transaction Id: 1 - executing, 2 - executing successfully, 3 - executing failed
     */
    private ConcurrentHashMap<String, Integer> transMap = new ConcurrentHashMap<>();

    /**
     * Execute local transactions
     *
     * @param msg
     * @param arg
     * @return
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        // Execute local transactions
        String transId = msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID).toString();
        log.info("Message transaction id by:" + transId);
        // Status is executing
        transMap.put(transId, 1);
        try {
            // Local SQL transactions must be executed in the callback method of MQ listener
            log.info("Executing local transaction");

            // Simulate the time-consuming operation and estimate the starting mq backcheck operation: when RocketMQ does not receive the return result of the local transaction for a long time (1 minute)
            // TimeUnit.SECONDS.sleep(80);

            // Simulate code execution, such as inserting user data into the database and failure
            // System.out.println(1 / 0);

            log.info("Transaction execution completed.");
        } catch (Exception e) {
            // The status is execution failed
            transMap.put(transId, 3);
            log.error("Transaction execution exception.");

            // An exception occurred
            // Set to: ROLLBACK if no retry is required
            // If the transaction needs to be checked and retried, and the check is initiated after 1 minute, it is set to UNKNOWN
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        // The status is execution success
        transMap.put(transId, 2);
        return RocketMQLocalTransactionState.COMMIT;
    }


    /**
     * Transaction timeout, check back method
     * Check the local transaction. If RocketMQ does not receive the return result of the local transaction for a long time (about 1 minute), it will regularly take the initiative to execute the modified method to query the execution of the local transaction.
     *
     * @param msg
     * @return
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {

        //Check the status of the transaction according to the id of the transaction and return it to the message queue
        //Unknown status: the transaction status is queried, but there is always no result, or the sending is unsuccessful due to network reasons. It is an unknown status for mq
        //Submit correctly and return localtransactionstate COMMIT_ MESSAGE
        //Transaction execution failed, and localtransactionstate is returned ROLLBACK_ MESSAGE
        String transId = (String) msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
        Integer status = transMap.get(transId);
        // Execution status: 1-executing, 2-executing successfully, 3-executing failed
        log.info("Back checked transactions id by:" + transId + ",The current status is:" + status);
        //Executing
        if (status == 1) {
            log.info("The recheck result is: executing status");
            return RocketMQLocalTransactionState.UNKNOWN;
        } else if (status == 2) {
            //If the execution is successful, return commit
            log.info("The recheck result is: success status");
            transMap.remove(transId);
            return RocketMQLocalTransactionState.COMMIT;
        } else if (status == 3) {
            //Execution failed, return rollback
            log.info("The recheck result is: failure status");
            return RocketMQLocalTransactionState.ROLLBACK;
            // Check local transaction execution through pseudo code representation
            // User user = selectByUserId(userId);
            // if (user!=null) {
            //     //Insert succeeded (local transaction completed)
            //     transMap.remove(transId);
            //     return RocketMQLocalTransactionState.COMMIT;
            // } else {
            //      //Insert failed
            //      //If you do not need to retry again, set it to: ROLLBACK
            //      //If you still need to check the transaction retry, set it to: UNKNOWN
            //     return RocketMQLocalTransactionState.UNKNOWN;
            // }
        }

        //  In other unknown cases, the message will be deleted without retry
        transMap.remove(transId);
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

TxProducer:

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.UUID;


@Service
@Slf4j
public class TxProducer {
    /**
     * A RocketMQTemplate can only register one transaction listener,
     * If there are multiple transaction listeners listening to different 'producers',
     * Different rocketmqtemplates need to be defined through the annotation ` @ ExtRocketMQTemplateConfiguration '
     */
    @Resource(name = "txRocketMQTemplate")
    RocketMQTemplate rocketMQTemplate;

    public void tx() {
        String text = "Message transaction sending" + System.currentTimeMillis();
        log.info(text);
        UUID transactionId = UUID.randomUUID();
        log.info("affair ID: " + transactionId);
        Message<String> message = MessageBuilder.withPayload(text)
                // Set transaction Id
                .setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId)
                .build();
        // After investigating sendMessageInTransaction, proceed to the listener
        rocketMQTemplate.sendMessageInTransaction("tx_topic", message, null);
        log.info("has been sent...");
    }
}

Topics: Java Distribution message queue RocketMQ