1. Principle of transaction message
The implementation principle of RocketMQ transaction message is based on two-stage commit and scheduled transaction status check to determine whether the message is finally committed or rolled back.
2. Transaction message start
TransactionListener transactionListener = new TransactionListenerImpl(); TransactionMQProducer producer = new TransactionMQProducer("trans_producer"); ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("client-transaction-msg-check-thread"); return thread; } }); // The thread pool that executes the transaction listening task producer.setExecutorService(executorService); producer.setTransactionListener(transactionListener); producer.start();
2.1 transaction producer startup
Call org.apache.rocketmq.client.producer.TransactionMQProducer#start: first, initialize the basic configuration of the transaction producer, then call org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#start: complete the startup of the producer. The logic here is the same as the default producer startup logic.
public void start() throws MQClientException { // Initialize the transaction message environment and set the thread pool to listen for transaction messages this.defaultMQProducerImpl.initTransactionEnv(); // Start the default producer process super.start(); } public void initTransactionEnv() { // Thread pool initialization for transaction state execution TransactionMQProducer producer = (TransactionMQProducer) this.defaultMQProducer; if (producer.getExecutorService() != null) { this.checkExecutor = producer.getExecutorService(); } else { this.checkRequestQueue = new LinkedBlockingQueue<Runnable>(2000); this.checkExecutor = new ThreadPoolExecutor( 1, 1, 1000 * 60, TimeUnit.MILLISECONDS, this.checkRequestQueue); } }
3 send transaction message
org.apache.rocketmq.client.producer.TransactionMQProducer#sendMessageInTransaction: the transaction producer's interface for sending messages. First, check whether the event listener is registered. If it is empty, throw the legacy directly to the. Finally, call org.apache.rocketmq.client.impl.producer.defaultmqproducer impl#sendmessageintransaction: send synchronously by default
public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) throws MQClientException { if (null == this.transactionListener) { throw new MQClientException("TransactionListener is null", null); } return this.defaultMQProducerImpl.sendMessageInTransaction(msg, transactionListener, arg); }
3.1 transaction message sending process
-
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendMessageInTransaction: to send a transaction message, first check whether the transaction executor exists, then check whether the message is legal, and then add a message to the attribute: TRAN_MSG:true,PGROUP:groupName.
// If the transaction executor is empty, an exception is thrown if (null == tranExecuter) { throw new MQClientException("tranExecutor is null", null); } // Check message Validators.checkMessage(msg, this.defaultMQProducer); SendResult sendResult = null; // Set msg attribute, TRAN_MSG:true MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); // PGROUP:groupName MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup()); try {
-
Then call org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#send): the sending message logic of the default producer is analyzed above. For the transaction message, the main message is to set the type of message to Trans_. Msg_ Half message, then call org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#prepareMessage: broker to receive the message, then process the prepare message. Other logic is the same as general messages.
// Transaction message processing final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag()); // Transaction commit or no operation if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) { // Delay delivery, and ensure that the maximum delay level is maxDelayLevel if (msg.getDelayTimeLevel() > 0) { if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) { msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()); } // When a message is stored, the delayed message entry Topic is SCHEDULE_TOPIC_XXXX topic = ScheduleMessageService.SCHEDULE_TOPIC; // The delay level is fixed mapped with the message queue number: queueId = delayLevel - 1 queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel()); // Back up the real topic and message queue id into the attribute MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic()); MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId())); msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties())); // Set the modified topic and queueId to ensure that messages of the same message level will be placed in the same queue msg.setTopic(topic); msg.setQueueId(queueId); } }
-
After obtaining the message sending result of the previous step, perform the corresponding operation.
- After sending successfully, set the transactionId of the message, and then execute org.apache.rocketmq.client.producer.transactionlistener#executelocal transaction: the logic of the local transaction in the transaction listener. Get the local transaction status of the transaction message. This is to ensure that the local transaction and the business code are in the same transaction, that is, the transaction consistency on the sender side.
- Sending failed: set the transaction status to ROLLBACK_MESSAGE.
// Initialize local transaction status to unknown LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW; Throwable localException = null; switch (sendResult.getSendStatus()) { case SEND_OK: { try { // Set transaction id if (sendResult.getTransactionId() != null) { msg.putUserProperty("__transactionId__", sendResult.getTransactionId()); } // Gets the transaction id of the message client String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); if (null != transactionId && !"".equals(transactionId)) { msg.setTransactionId(transactionId); } // 2. If the message is sent successfully, process the local transaction unit associated with the message and listen to the transaction message implemented by the client itself localTransactionState = tranExecuter.executeLocalTransaction(msg, arg); // If the returned local transaction state is not commit, it will be rolled back if (null == localTransactionState) { localTransactionState = LocalTransactionState.UNKNOW; } if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) { log.info("executeLocalTransactionBranch return {}", localTransactionState); log.info(msg.toString()); } } catch (Throwable e) { log.info("executeLocalTransactionBranch exception", e); log.info(msg.toString()); localException = e; } } break; case FLUSH_DISK_TIMEOUT: case FLUSH_SLAVE_TIMEOUT: case SLAVE_NOT_AVAILABLE: localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE; break; default: break; }
-
Call org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#endTransaction: end the transaction, and perform commit, rollback or temporarily not processing operations according to the transaction status obtained in the previous step. Finally, send a request to the broker to end the transaction. According to the transaction status, commit or rollback the progress of the commitlog file and whether to delete the prepare message.
public void endTransaction( final SendResult sendResult, final LocalTransactionState localTransactionState, final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException { //1. Find the Prepared message according to sendResult, which contains the ID of the transaction message //2. Update the final status of the message according to localTransaction final MessageId id; if (sendResult.getOffsetMsgId() != null) { id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId()); } else { id = MessageDecoder.decodeMessageId(sendResult.getMsgId()); } String transactionId = sendResult.getTransactionId(); // Find the broker address to send the message request final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName()); EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader(); requestHeader.setTransactionId(transactionId); // commilog file offset requestHeader.setCommitLogOffset(id.getOffset()); switch (localTransactionState) { case COMMIT_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE); break; case ROLLBACK_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE); break; case UNKNOW: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE); break; default: break; } requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); requestHeader.setTranStateTableOffset(sendResult.getQueueOffset()); requestHeader.setMsgId(sendResult.getMsgId()); String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null; // Send request, one-way send this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark, this.defaultMQProducer.getSendMsgTimeout()); }
3 commit or rollback transaction
The commit or rollback of a transaction depends on the result returned after the execution of the local transaction logic. When the result is COMMIT_MESSAGE, ROLLBACK_ When message or unknown, different processing will be carried out.
3.1 transaction processing
No matter what the result of the local transaction is, the client will send a one-way request to the broker of the master node. After receiving the request, the broker will forward it to EndTransactionProcessor for processing according to the request code. Call org.apache.rocketmq.broker.processor.EndTransactionProcessor#processRequest to process the transaction results.
-
Check whether the role of store is in save mode. If yes, return directly.
// If the store is in the slave mode, it returns directly if (BrokerRole.SLAVE == brokerController.getMessageStoreConfig().getBrokerRole()) { response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE); LOGGER.warn("Message store is slave mode, so end transaction is forbidden. "); return response; }
-
If the transaction status is TRANSACTION_NOT_TYPE, null will be returned directly without processing
switch (requestHeader.getCommitOrRollback()) { // Return null without operation case MessageSysFlag.TRANSACTION_NOT_TYPE: { LOGGER.warn("The producer[{}] end transaction in sending message, and it's pending status." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), request.getRemark()); return null; } ......... ......... }
-
If the transaction status is TRANSACTION_COMMIT_TYPE: indicates that the local transaction is executed successfully. First, obtain the prepare message from the commitlog file according to the offset. If the prepare message is obtained successfully, check whether the prepare message is legal. Finally, use the prepare message to restore the original topic, queueId and other information to build the message, send the final message and delete the prepare message.
if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) { // Get the prepare message according to the offset from the commitlog file result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader); // Get prepare message succeeded if (result.getResponseCode() == ResponseCode.SUCCESS) { // Check whether the prepare message is legal RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); if (res.getCode() == ResponseCode.SUCCESS) { // Why use the prepare message? Because the prepare message will not be consumed, it will exist until the final message is sent, // Use the prepare message, re-use the original topic and queueId to build the message and send the message. Finally, delete the prepare message, MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage()); msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback())); msgInner.setQueueOffset(requestHeader.getTranStateTableOffset()); msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset()); msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp()); // Sending the final message is actually restoring the previously modified message RemotingCommand sendResult = sendFinalMessage(msgInner); // Delete the prepare message, and then if (sendResult.getCode() == ResponseCode.SUCCESS) { this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); } return sendResult; } return res; } }
-
If the transaction status is TRANSACTION_ROLLBACK_TYPE: indicates that the local transaction failed to execute. First, obtain the prepare message from the commitlog file according to the offset. If the prepare message is obtained successfully, check whether the prepare message is legal. Finally, delete the prepare message directly. Here is to ensure that the first stage is atomic.
if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) { // Get the prepare message according to the offset from the commitlog file result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader); // Get prepare message succeeded if (result.getResponseCode() == ResponseCode.SUCCESS) { // Check whether the prepare message is legal RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); if (res.getCode() == ResponseCode.SUCCESS) { // If the rollback operation is performed, the prepare message will be deleted directly. this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); } return res; } }
-
The deletion mentioned in the above two transaction States is not a real deletion. Call org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#deletePrepareMessage: store the prepare message in RMQ_ SYS_ TRANS_ OP_ HALF_ In topic topic, it indicates that the transaction message (message in prepare status) has been processed, which provides a search basis for the transaction backcheck of unprocessed transactions.
public boolean deletePrepareMessage(MessageExt msgExt) { if (this.transactionalMessageBridge.putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG)) { log.info("Transaction op message write successfully. messageId={}, queueId={} msgExt:{}", msgExt.getMsgId(), msgExt.getQueueId(), msgExt); return true; } else { log.error("Transaction op message write failed. messageId is {}, queueId is {}", msgExt.getMsgId(), msgExt.getQueueId()); return false; } }
3.2 transaction review
RocketMQ detects RMQ through the TransactionalMessageCheckService thread_ SYS_ TRANS_ For messages in the half topic topic, the detection frequency of the transaction status of the message transaction check service is 1 minute by default. The default value can be changed by setting the transactionchecklninterval in the broker.conf file, and the unit is milliseconds.
-
The transaction status here is TRANSACTION_COMMIT_TYPE,TRANSACTION_ ROLLBACK_ The logic of type has been processed when the status is transaction_ NOT_ If the type and prepare messages have not been deleted, they will be sent to the transaction callback service TransactionalMessageCheckService to check the transaction status, that is, the user-defined transaction callback interface. This service is an org.apache.rocketmq.broker.brokercontroller#initialtransaction transaction. It is a transaction callback service started when the broker controller is initialized.
public void start() { // started defaults to false if (started.compareAndSet(false, true)) { // Run run method super.start(); this.brokerController.getTransactionalMessageService().open(); } } public void run() { log.info("Start transaction check service thread!"); // The default check interval is 60 * 1000 = 1s long checkInterval = brokerController.getBrokerConfig().getTransactionCheckInterval(); while (!this.isStopped()) { // implement this.waitForRunning(checkInterval); } log.info("End transaction check service thread!"); }
-
org.apache.rocketmq.broker.transaction.TransactionalMessageCheckService#onWaitEnd: check the transaction status of the message.
protected void onWaitEnd() {
//The transaction expiration time is 3s by default
long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
//The maximum number of transaction backchecks is 5 by default
int checkMax = brokerController.getBrokerConfig().getTransactionCheckMax();
long begin = System.currentTimeMillis();
log.info("Begin to check prepare message, begin time:{}", begin);
this.brokerController.getTransactionalMessageService().check(timeout, checkMax, this.brokerController.getTransactionalMessageCheckListener());
log.info("End to check prepare message, consumed time:{}", System.currentTimeMillis() - begin);
}3. `org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#Check `: the core method to check the transaction status of messages through the topic ` RMQ_SYS_TRANS_HALF_TOPIC ` get the message queue, that is, the queue where the prepare message is located. ```java // The subject that handles the prepare message String topic = MixAll.RMQ_SYS_TRANS_HALF_TOPIC; // Get the routing information and message queue of topic from the broker Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic); if (msgQueues == null || msgQueues.size() == 0) { log.warn("The queue of topic is empty :" + topic); return; }
-
Traverse the obtained message queue to obtain the prepared message that has been processed, with the subject of SYS_TRANS_OP_HALF_TOPIC, that is, the first stage is a message whose transaction status is commit and rollback. Then get the prepare message progress and the processed prepare message progress on the same message queue respectively.
for (MessageQueue messageQueue : msgQueues) { long startTime = System.currentTimeMillis(); // Gets the message subject RMQ sys of the processed prepare message_ TRANS_ OP_ HALF_ TOPIC MessageQueue opQueue = getOpQueue(messageQueue); // Get the consumption progress of prepare message long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue); // Gets the message progress of the processed prepare message long opOffset = transactionalMessageBridge.fetchConsumeOffset(opQueue); log.info("Before check, the queue={} msgOffset={} opOffset={}", messageQueue, halfOffset, opOffset); if (halfOffset < 0 || opOffset < 0) { log.error("MessageQueue: {} illegal offset read: {}, op offset: {},skip this queue", messageQueue, halfOffset, opOffset); continue; } ....... ....... }
-
Call org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#fillOpRemoveMap: pull 32 messages from opQueue to judge whether the message has been processed. If it has been processed, you don't need to send transaction status check back request again to avoid repeated transaction check back.
List<Long> doneOpOffset = new ArrayList<>(); HashMap<Long, Long> removeMap = new HashMap<>(); // Pull 32 messages from the processed queue according to the current processing progress to judge whether the message has been processed, // If it is processed, it is unnecessary to send the transaction status check back request again to avoid repeated transaction check back PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset); // The pull message is empty. Continue to try to pull the next consumption queue if (null == pullResult) { log.error("The queue={} check msgOffset={} with opOffset={} failed, pullResult is null", messageQueue, halfOffset, opOffset); continue; } private PullResult fillOpRemoveMap(HashMap<Long, Long> removeMap, MessageQueue opQueue, long pullOffsetOfOp, long miniOffset, List<Long> doneOpOffset) { // Pull 32 messages from opQueue PullResult pullResult = pullOpMsg(opQueue, pullOffsetOfOp, 32); if (null == pullResult) { return null; } // Pull message status if (pullResult.getPullStatus() == PullStatus.OFFSET_ILLEGAL || pullResult.getPullStatus() == PullStatus.NO_MATCHED_MSG) { log.warn("The miss op offset={} in queue={} is illegal, pullResult={}", pullOffsetOfOp, opQueue, pullResult); transactionalMessageBridge.updateConsumeOffset(opQueue, pullResult.getNextBeginOffset()); return pullResult; } else if (pullResult.getPullStatus() == PullStatus.NO_NEW_MSG) { log.warn("The miss op offset={} in queue={} is NO_NEW_MSG, pullResult={}", pullOffsetOfOp, opQueue, pullResult); return pullResult; } List<MessageExt> opMsg = pullResult.getMsgFoundList(); if (opMsg == null) { log.warn("The miss op offset={} in queue={} is empty, pullResult={}", pullOffsetOfOp, opQueue, pullResult); return pullResult; } // Pull the processed prepare message successfully for (MessageExt opMessageExt : opMsg) { Long queueOffset = getLong(new String(opMessageExt.getBody(), TransactionalMessageUtil.charset)); log.info("Topic: {} tags: {}, OpOffset: {}, HalfOffset: {}", opMessageExt.getTopic(), opMessageExt.getTags(), opMessageExt.getQueueOffset(), queueOffset); if (TransactionalMessageUtil.REMOVETAG.equals(opMessageExt.getTags())) { if (queueOffset < miniOffset) { // A collection of messages that have been processed doneOpOffset.add(opMessageExt.getQueueOffset()); } else { // Message set to be removed key:halfOffset, value: opOffset removeMap.put(queueOffset, opMessageExt.getQueueOffset()); } } else { log.error("Found a illegal tag in opMessageExt= {} ", opMessageExt); } } // Progress of op messages to be removed log.debug("Remove map: {}", removeMap); // Message progress completed log.debug("Done op list: {}", doneOpOffset); return pullResult; }
-
Next, the pullResult result is processed. If the offset of the prepare message has been processed, it is removed from the collection and the next operation is performed.
// Number of times to get an empty message int getMessageNullCount = 1; // Currently processing RMQ_ SYS_ TRANS_ HALF_ Latest progress of topic #queueld long newOffset = halfOffset; // The queue offset of the currently processed message. The subject is still RMQ_SYS_TRANS_HALF_TOPIC long i = halfOffset; while (true) { // If the processing time is greater than 1min, it will stop if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) { log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT); break; } // The current message has been processed. Remove it from the collection and continue processing the next message if (removeMap.containsKey(i)) { log.info("Half offset {} has been committed/rolled back", i); removeMap.remove(i); } else { ..... ..... } newOffset = i + 1; i++; }
-
When the offset of the prepare message has not been processed, obtain the prepare message according to the offset and check whether the message is legal.
// Get the message from the consumption queue according to the message queue offset GetResult getResult = getHalfMsg(messageQueue, i); MessageExt msgExt = getResult.getMsg(); // The message is empty if (msgExt == null) { // When the pull message is empty, it will be retried once by default. If it exceeds, the transaction status check of the message queue will be ended if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) { break; } // No new news, just break if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) { log.info("No new msg, the miss offset={} in={}, continue check={}, pull result={}", i, messageQueue, getMessageNullCount, getResult.getPullResult()); break; } else { // i set to the next offset and pull again log.info("Illegal offset, the miss offset={} in={}, continue check={}, pull result={}", i, messageQueue, getMessageNullCount, getResult.getPullResult()); i = getResult.getPullResult().getNextBeginOffset(); newOffset = i; continue; } }
-
This means that the message has been checked to determine whether the message needs to be discarded or skipped. Discard criteria: if the number of message lookbacks is greater than the maximum allowed number of lookbacks, the attribute transaction of the message will be discarded_ CHECK_ Times plus 1, the default value is 5. Skip standard: the transaction message exceeds the expiration time of the file, and the default value is 72 hours. If you are sure to discard or skip, execute the next obtained message. Continue to judge whether the storage time of the message is greater than the current time. If it is greater than the current time, it means that the message is added after the backcheck. break directly.
// When the message is not empty, judge whether the message needs to be discarded (swallowed, discarded, not processed) or skipped // Judge discarding: if the number of message lookbacks is greater than the maximum allowed number of lookbacks, the attribute transaction of the message will be discarded_ CHECK_ Times plus 1, the default is 5 // Judge skip: the transaction message exceeds the expiration time of the file. The default value is 72 hours if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) { listener.resolveDiscardMsg(msgExt); newOffset = i + 1; i++; continue; } // Storage time > = start time, which indicates the messages added later. It is not processed here. break if (msgExt.getStoreTimestamp() >= startTime) { log.info("Fresh stored. the miss offset={}, check it later, store={}", i, new Date(msgExt.getStoreTimestamp())); break; }
-
Check whether the current message can execute the back check request, and judge the storage time of the message, the time of the first check method, the storage time and the immediate detection message.
// The time when the message has been stored, which is the current time of the system minus the time stamp of the message storage long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp(); // checkImmunityTime: the time to immediately detect the transaction message. transactionTimeout: the timeout time of the transaction message, which is 3s by default long checkImmunityTime = transactionTimeout; // The latest time of the message transaction message backcheck request. It is valid only when the backcheck message is received within this time. It is null by default String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS); if (null != checkImmunityTimeStr) { checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout); //Message stored time < check now time, wait for next retry if (valueOfCurrentMinusBorn < checkImmunityTime) { // If true is returned, the message is skipped if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt, checkImmunityTime)) { newOffset = i + 1; i++; continue; } } } else { // checkImmunityTime=transactionTimeout, the newly arrived message is not in the scope of this time. break directly if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) { log.info("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i, checkImmunityTime, new Date(msgExt.getBornTimestamp())); break; } }
-
Here, to indicate the final check, whether it is necessary to send the transaction back to the message, and confirm that after executing the request, call the asynchronous thread pool to send the transaction request.
- If there are no processed messages in the processed message queue and the application transaction end time has been exceeded, that is, transactionTimeout
- The processed message queue is not empty, and the storage time of the last message is greater than transactionTimeout.
// Determine whether to send a transaction query back message: // 1. If there is no processed message in the processed message queue and the application transaction end time has been exceeded, that is, transactionTimeout // 2. The processed message queue is not empty, and the storage time of the last message is greater than transactionTimeout. boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime) || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout)) || (valueOfCurrentMinusBorn <= -1); if (isNeedCheck) { // Put the message into RMQ first_ SYS_ TRANS_ HALF_ Topic topic, send successfully, true if (!putBackHalfMsgQueue(msgExt, i)) { continue; } // Call asynchronous thread pool to send transaction lookup command listener.resolveHalfMsg(msgExt); } else { // Obtain the processed message progress again for subsequent filtering of duplicate processing data pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset); log.info("The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}", i, messageQueue, pullResult); continue; }
-
Finally, update the prepare message consumption queue and the processed prepare message queue.
// Indicates that there are multiple messages. Update the consumption queue if (newOffset != halfOffset) { transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset); } // Recalculate the consumption progress of processed messages long newOpOffset = calculateOpOffset(doneOpOffset, opOffset); // Indicates that the processed message progress is increased and the consumption queue is updated if (newOpOffset != opOffset) { transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset); }
-
3.3 send transaction back query
org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener#resolveHalfMsg: asynchronous thread pool sends backlookup request, org.apache.rocketmq.broker.transaction.AbstractTransactionalMessageCheckListener#sendCheckMessage: sends backlookup request.
public void sendCheckMessage(MessageExt msgExt) throws Exception { CheckTransactionStateRequestHeader checkTransactionStateRequestHeader = new CheckTransactionStateRequestHeader(); // Encapsulate the callback request header checkTransactionStateRequestHeader.setCommitLogOffset(msgExt.getCommitLogOffset()); checkTransactionStateRequestHeader.setOffsetMsgId(msgExt.getMsgId()); checkTransactionStateRequestHeader.setMsgId(msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX)); checkTransactionStateRequestHeader.setTransactionId(checkTransactionStateRequestHeader.getMsgId()); checkTransactionStateRequestHeader.setTranStateTableOffset(msgExt.getQueueOffset()); msgExt.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC)); msgExt.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID))); msgExt.setStoreSize(0); // Get production group id String groupId = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); // Obtain the available producers according to the producer group id Channel channel = brokerController.getProducerManager().getAvaliableChannel(groupId); if (channel != null) { // When processing the transaction check back, the method org.apache.rocketmq.client.producer.TransactionListener.checkLocalTransaction defined by the producer will be called // According to the committed transaction results, another phase of transaction processing is performed, including commit, rollback or no processing. brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt); } else { LOGGER.warn("Check transaction failed, channel is null. groupId={}", groupId); } }