Final consistency of reliable messages [local message table, RocketMQ transaction message scheme]

Posted by scottd on Fri, 28 Jan 2022 23:11:41 +0100

Excerpt from: https://www.cnblogs.com/zhengzhaoxiang/p/13976517.html

1, Reliable message final consistency transaction overview

The final consistency scheme of reliable message means that when the transaction initiator executes and completes the local transaction and sends a message, the transaction participants (message consumers) will be able to receive the message and process the transaction successfully. This scheme emphasizes that the final transaction must be consistent as long as the message is sent to the transaction participants. This scheme is completed by using message oriented middleware, as shown in the following figure:

The transaction initiator (message producer) sends the message to the message middleware, and the transaction participant receives the message from the message middleware. The transaction participant (message consumer) and the message middleware communicate through the network. The uncertainty of network communication will lead to distributed transaction problems. Therefore, the final consistency scheme of reliable messages should solve the following problems:
[1] Atomicity of local transaction and message sending: after the local transaction is successfully executed, the transaction initiator must send the message, otherwise it will discard the message. That is, the atomicity of local transactions and message sending is either successful or failed. The atomicity of local transactions and message sending is the key problem to realize the final consistency scheme of reliable messages. Try this operation first, send the message first, and then operate the database: in this case, the consistency between the database operation and the sent message cannot be guaranteed, because the message may be sent successfully and the database operation may fail.

1 begin transaction; 
2     //1. Send MQ 
3     //2. Database operation 
4 commit transation;

The second scheme is to operate the database first and then send the message: there seems to be no problem in this case. If the sending of MQ message fails, an exception will be thrown, resulting in the rollback of database transactions. However, if it is a timeout exception and the database is rolled back, but MQ has actually been sent normally, which will also lead to inconsistency.

1 begin transaction; 
2     //1. Database operation 
3     //2. Send MQ 
4 commit transation;

[2] Reliability of receiving messages by transaction participants: transaction participants must be able to receive messages from the message queue. If receiving messages fails, they can receive messages repeatedly.
[3] Problem of repeated consumption of messages: due to the existence of step 2, if a consumption node times out but the consumption is successful, the message middleware will deliver the message repeatedly, resulting in repeated consumption of messages. To solve the problem of repeated consumption of messages, it is necessary to realize the method idempotency of transaction participants.

2, Solution [local message table]

The scheme of local message table was originally proposed by eBay. The core of this scheme is to ensure the consistency of data business operations and messages through local transactions, and then send the message to the message middleware through a scheduled task. After confirming that the message is sent to the consumer successfully, delete the message. Let's take the registration of points as an example: in the following example, there are two micro service interactions, user service and point service. User service is responsible for adding users, and point service is responsible for increasing points.

[interaction process is as follows]: [1] user registration: user services add users and "integral message log" in local transactions. (the consistency between the user table and the message table is guaranteed through the local transaction) the following is the pseudo code. In this case, the local database operation and the stored integral message log are in the same transaction, and the local database operation and the recorded message log operation are atomic.

1 begin transaction; 
2     //1. New users 
3     //2. Store integral message log 
4 commit transation;

[2] Scheduled task scanning log: how to ensure that messages are sent to the message queue? After the first step, the message has been written to the message log table. You can start an independent thread to scan the message in the message log table regularly and send it to the message middleware. After the message middleware feeds back that the message log is sent successfully, delete the message log, otherwise wait for the next cycle of the scheduled task to retry.
[3] Consumer news: how to ensure that consumers can consume news? Here, the ACK (message confirmation) mechanism of MQ can be used. The consumer listens to MQ. If the consumer receives the message and sends an ACK (message confirmation) to MQ after the business processing is completed, it indicates that the consumer's normal consumption message is completed, and MQ will no longer push the message to the consumer, otherwise the consumer will continue to retry sending the message to the consumer. The point service receives the message of "increasing points" and starts to increase points. After the point is increased successfully, it responds to ack to the message middleware, otherwise the message middleware will deliver the message repeatedly. Since the message will be delivered repeatedly, the "increasing points" function of the point service needs to realize idempotency.

3, Solution [RocketMQ transaction message solution]

RocketMQ is a distributed messaging middleware from Alibaba. It was open source in 2012 and officially became the top project of Apache in 2017. It is understood that, including Alibaba cloud's messaging products and acquired subsidiaries, Alibaba Group's messaging products run on RocketMQ, and RocketMQ has made eye-catching performance in the promotion of the double 11 National Congress in recent years. Apache RocketMQ version after 4.3 officially supports transaction messages, which provides convenient support for the implementation of distributed transactions. The design of RocketMQ transaction message is mainly to solve the atomicity problem between the message sending on the Producer side and the execution of local transactions. In the design of RocketMQ, the two-way communication ability between the broker and the Producer side makes the broker naturally exist as a transaction coordinator; The storage mechanism provided by RocketMQ itself provides the persistence ability for transaction messages; RocketMQ's high availability mechanism and reliable message design are that transaction messages can still ensure the final consistency of transactions in case of system exceptions. After RocketMQ 4.3, the complete transaction message is implemented. In fact, it is an encapsulation of the local message table. The local message table is moved to the interior of MQ to solve the atomicity problem between message sending on the Producer side and local transaction execution.

[execution process is as follows]: to facilitate understanding, we also describe the whole process with the example of registering and sending points. Producer is the MQ sender. In this case, it is a user service, which is responsible for adding users. The MQ subscriber is the message consumer. In this case, it is the point service, which is responsible for adding points.
[1] Producer sends transaction message: Producer (MQ sender) sends transaction message to MQ Server, and MQ Server marks the message status as Prepared. Note that this message cannot be consumed by consumers (MQ subscribers) at this time. In this example, producer sends the "add points message" to MQ Server.
[2] MQ Server response message sent successfully: if MQ Server receives the message sent to by Producer, the response is sent successfully. Indicates that MQ has received a message.
[3] Producer executes local transactions: the producer side executes business code logic and is controlled through local database transactions. In this example, producer performs the add user operation.
[4] Message delivery: if the} Producer local transaction is executed successfully, it will automatically send a commit message to the MQServer. After receiving the commit message, the MQ Server will mark the "add points message" status as consumable. At this time, the MQ subscriber (points service) will consume the message normally. If the Producer # local transaction fails to execute, it will automatically send a Rollback message to MQServer. After receiving the Rollback message, MQ Server will delete the "add points message". The MQ subscriber (point service) consumes the message. If the consumption is successful, it will respond with ack to MQ, otherwise it will receive the message repeatedly. Here, ACK will respond automatically by default, that is, if the program runs normally, ACK will be responded automatically.
[5] Transaction check back: if the execution end hangs up or times out during the local transaction of the Producer, MQ Server will keep asking other producers in the same group to obtain the transaction execution status. This process is called transaction check back. MQ Server will decide whether to post the message according to the transaction backcheck results. The above backbone processes have been implemented by RocketMQ. For the user side, the user needs to implement local transaction execution and local transaction backcheck methods respectively. Therefore, only pay attention to the execution status of local transactions (maintain the local transaction status table). RoacketMQ provides rocketmqllocaltransactionlistener interface:

 1 public interface RocketMQLocalTransactionListener {
 2     /**The prepare message is sent successfully. This method is called back. This method is used to execute local transactions 
 3     * @param msg The unique Id of the returned message can be obtained by using transactionId
 4     * @param arg Parameters passed when calling the send method. If additional parameters can be passed to the send method when sending, you can get them here
 5     * @return Return transaction status, COMMIT: COMMIT ROLLBACK: ROLLBACK unknown: callback 
 6     */
 7     RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg); 
 8      
 9      /**@param msg Determine the local transaction execution status of this message by obtaining the transactionId 
10       * @return Return transaction status, COMMIT: COMMIT ROLLBACK: ROLLBACK unknown: callback 
11       */
12     RocketMQLocalTransactionState checkLocalTransaction(Message msg); 
13  }

[6] Send transaction message: the following is the API provided by RocketMQ for sending transaction message:

1 TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup"); 
2 producer.setNamesrvAddr("127.0.0.1:9876"); 
3 producer.start(); 
4 //Set TransactionListener implementation 
5 producer.setTransactionListener(transactionListener); 
6 //Send transaction message 
7 SendResult sendResult = producer.sendMessageInTransaction(msg, null);

4, RocketMQ implements reliable message final consistency transaction

[business description] through RocketMQ middleware, reliable messages and ultimately consistent distributed transactions are realized to simulate the transfer transaction process of two accounts. The two accounts are in different banks (Zhang San in bank1 and Li Si in bank2). Bank1 and bank2 are two micro services. The transaction process is that Zhang San transfers the specified amount to Li Si. In the above transaction steps, Zhang San deducts the amount and sends a transfer message to bank2. The two operations must be an integrated transaction.


[core code]: the technical framework of the program is as follows:

[the interaction process is as follows]: [1] Bank1 sends a transfer message to MQ Server;
[2] Bank1 executes local affairs and deducts the amount;
[3] Bank2 receives messages, executes local transactions and adds amounts;
[database]: add {De to bank1 and bank2 databases_ Duplication, transaction record table (de duplication table), used for transaction idempotent control.

1 DROP TABLE IF EXISTS `de_duplication`; 
2 CREATE TABLE `de_duplication` ( 
3     `tx_no` varchar(64) COLLATE utf8_bin NOT NULL, 
4     `create_time` datetime(0) NULL DEFAULT NULL, 
5     PRIMARY KEY (`tx_no`) USING BTREE 
6 ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

[version dependency]: Specifies the version of rocketmq spring boot starter in the parent project

1 <dependency>
2     <groupId>org.apache.rocketmq</groupId>
3     <artifactId>rocketmq-spring-boot-starter</artifactId>
4     <version>2.0.2</version>
5 </dependency>

[configure rocketMQ]: in application local Configure rocketMQ nameServer address and production group in propertis.

1 rocketmq.producer.group = producer_bank2
2 rocketmq.name-server = 127.0.0.1:9876

[Zhang San service layer code]:

 1 import com.alibaba.fastjson.JSONObject;
 2 import org.apache.rocketmq.spring.core.RocketMQTemplate;
 3 import org.springframework.beans.factory.annotation.Autowired;
 4 import org.springframework.messaging.Message;
 5 import org.springframework.messaging.support.MessageBuilder;
 6 import org.springframework.stereotype.Service;
 7 import org.springframework.transaction.annotation.Transactional;
 8 
 9 /**
10  * @author Administrator
11  * @version 1.0
12  **/
13 @Service
14 @Slf4j
15 public class AccountInfoServiceImpl implements AccountInfoService {
16 
17     @Autowired
18     AccountInfoDao accountInfoDao;
19 
20     @Autowired
21     RocketMQTemplate rocketMQTemplate;
22 
23 
24     //Send transfer message to mq
25     @Override
26     public void sendUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
27 
28         //Convert accountChangeEvent to json
29         JSONObject jsonObject =new JSONObject();
30         jsonObject.put("accountChange",accountChangeEvent);
31         String jsonString = jsonObject.toJSONString();
32         //Generate message type
33         Message<String> message = MessageBuilder.withPayload(jsonString).build();
34         //Send a transaction message
35         /**
36          * String txProducerGroup production team
37          * String destination topic,
38          * Message<?> message, Message content
39          * Object arg parameter
40          */
41         rocketMQTemplate.sendMessageInTransaction("producer_group_txmsg_bank1","topic_txmsg",message,null);
42 
43     }
44 
45     //Update the account and deduct the amount
46     @Override
47     @Transactional
48     public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
49         //Idempotent judgment: txNo is the UUID generated in the Ctroller, which is globally unique
50         if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){
51             return ;
52         }
53         //Deduction amount
54         accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount() * -1);
55         //Add transaction log
56         accountInfoDao.addTx(accountChangeEvent.getTxNo());
57         if(accountChangeEvent.getAmount() == 3){
58             throw new RuntimeException("Artificial anomaly");
59         }
60     }
61 }

[Zhang San rocketmqllocaltransactionlistener]: write the interface implementation class of rocketmqllocaltransactionlistener to implement the two methods of executing local transactions and transaction backcheck.

 1 import com.alibaba.fastjson.JSONObject;
 2 import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
 3 import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
 4 import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
 5 import org.springframework.messaging.Message;
 6 import org.springframework.transaction.annotation.Transactional;
 7 
 8 /**
 9  * @author Administrator
10  * @version 1.0
11  **/
12 @Component
13 @Slf4j
14 //The producer group is the same as the group defined when sending the message
15 @RocketMQTransactionListener(txProducerGroup = "producer_group_txmsg_bank1")
16 public class ProducerTxmsgListener implements RocketMQLocalTransactionListener {
17 
18     @Autowired
19     AccountInfoService accountInfoService;
20 
21     @Autowired
22     AccountInfoDao accountInfoDao;
23 
24     //The callback method after the transaction message is sent. When the message is sent to mq successfully, this method is called back
25     @Override
26     @Transactional
27     public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
28 
29         try {
30             //Parse the message and convert it to AccountChangeEvent
31             String messageString = new String((byte[]) message.getPayload());
32             JSONObject jsonObject = JSONObject.parseObject(messageString);
33             String accountChangeString = jsonObject.getString("accountChange");
34             //Convert accountChange (json) to AccountChangeEvent
35             AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
36             //Execute local transactions and deduct amount
37             accountInfoService.doUpdateAccountBalance(accountChangeEvent);
38             //When returning rocketmqllocaltransactionstate Commit, automatically send a commit message to mq, and mq changes the status of the message to consumable
39             return RocketMQLocalTransactionState.COMMIT;
40         } catch (Exception e) {
41             e.printStackTrace();
42             return RocketMQLocalTransactionState.ROLLBACK;
43         }
46     }
47 
48     //Check back the transaction status to query whether to deduct the amount
49     @Override
50     public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
51         //Parse the message and convert it to AccountChangeEvent
52         String messageString = new String((byte[]) message.getPayload());
53         JSONObject jsonObject = JSONObject.parseObject(messageString);
54         String accountChangeString = jsonObject.getString("accountChange");
55         //Convert accountChange (json) to AccountChangeEvent
56         AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
57         //Transaction id
58         String txNo = accountChangeEvent.getTxNo();
59         int existTx = accountInfoDao.isExistTx(txNo);
60         if(existTx>0){
61             return RocketMQLocalTransactionState.COMMIT;
62         }else{
63             return RocketMQLocalTransactionState.UNKNOWN;
64         }
65     }
66 }

[Li Si service layer code]:

 1 import org.springframework.stereotype.Service;
 2 import org.springframework.transaction.annotation.Transactional;
 3 
 4 /**
 5  * @author Administrator
 6  * @version 1.0
 7  **/
 8 @Service
 9 @Slf4j
10 public class AccountInfoServiceImpl implements AccountInfoService {
11 
12     @Autowired
13     AccountInfoDao accountInfoDao;
14 
15     //Update the account and increase the amount
16     @Override
17     @Transactional
18     public void addAccountInfoBalance(AccountChangeEvent accountChangeEvent) {
19         log.info("bank2 Update local account, account:{},amount of money:{}",accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
20         if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){
21             return ;
22         }
23         //Increase amount
24         accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
25         //Add transaction record for idempotent
26         accountInfoDao.addTx(accountChangeEvent.getTxNo());
27         if(accountChangeEvent.getAmount() == 4){
28             throw new RuntimeException("Artificial anomaly");
29         }
30     }
31 }

[MQ listening class]: listen to the target Topic by implementing the RocketMQListener interface

 1 import com.alibaba.fastjson.JSONObject;
 2 import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
 3 import org.apache.rocketmq.spring.core.RocketMQListener;
 4 
 5 /**
 6  * @author Administrator
 7  * @version 1.0
 8  **/
 9 @Component
10 @Slf4j
11 @RocketMQMessageListener(consumerGroup = "consumer_group_txmsg_bank2",topic = "topic_txmsg")
12 public class TxmsgConsumer implements RocketMQListener<String> {
13 
14     @Autowired
15     AccountInfoService accountInfoService;
16 
17     //receive messages
18     @Override
19     public void onMessage(String message) {
20         log.info("Start consumption message:{}",message);
21         //Parse message
22         JSONObject jsonObject = JSONObject.parseObject(message);
23         String accountChangeString = jsonObject.getString("accountChange");
24         //Convert to AccountChangeEvent
25         AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
26         //Set the account number as Li Si
27         accountChangeEvent.setAccountNo("2");
28         //Update local account and increase amount
29         accountInfoService.addAccountInfoBalance(accountChangeEvent);
31     }
32 }

5, Summary

The final consistency of reliable messages is to ensure the consistency of messages transmitted from the manufacturer to the consumer through message middleware. In this case, RocketMQ is used as message middleware. RocketMQ mainly solves two functions:
[1] Atomicity of local transactions and message sending;
[2] The reliability of the message received by the transaction participants;
Reliable message final consistency transaction is suitable for scenarios with long execution cycle and low real-time requirements. After the introduction of message mechanism, the synchronous transaction operation becomes asynchronous operation based on message execution, which avoids the influence of synchronous blocking operation in distributed transactions, and realizes the decoupling of the two services.