The transaction message of rocketmq is used as a solution to distributed transactions on some occasions. Today, let's analyze the transaction message of rocketmq
catalogue
1. Transaction message flow process
2. The producer sends a transaction message (half message)
3. The broker handles the half message
4. The producer executes a local transaction and returns the transaction execution result
5. The broker handles the local transaction results of the producer
five point three Rollback transaction
6. Delete the half message process
7. Review the implementation results of the transaction
1. Transaction message flow processExample code:
public static void main(String[] args) throws MQClientException { logger.info("producer start ..."); TransactionMQProducer producer = new TransactionMQProducer("ProducerGroupName1"); producer.setNamesrvAddr("127.0.0.1:9876");//Internet address producer.start(); producer.setTransactionListener(new TransactionListener() { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { logger.info("Execute local transactions"); return LocalTransactionState.COMMIT_MESSAGE; } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { logger.info("Check local transactions"); return LocalTransactionState.COMMIT_MESSAGE; } }); Message message = new Message("TopicTest3", "TAGA", "Test transaction message".getBytes()); producer.sendMessageInTransaction(message, null); producer.shutdown(); }2. The producer sends a transaction message (half message)
Note that the producer of transaction messages becomes TransactionMQProducer
public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) throws MQClientException { // transactionListener cannot be empty if (null == this.transactionListener) { throw new MQClientException("TransactionListener is null", null); } msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic())); return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg); }
public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter localTransactionExecuter, final Object arg) throws MQClientException { // Check the listener TransactionListener transactionListener = getCheckListener(); if (null == localTransactionExecuter && null == transactionListener) { throw new MQClientException("tranExecutor is null", null); } // ignore DelayTimeLevel parameter // Transaction messages do not support delay. Clear the delay message flag if (msg.getDelayTimeLevel() != 0) { MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL); } Validators.checkMessage(msg, this.defaultMQProducer); SendResult sendResult = null; // Add transaction flag bit MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); // Save produceGroup MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup()); try { // Send messages synchronously and wait for the results. arg will not be sent to the server sendResult = this.send(msg); } catch (Exception e) { // An exception occurs when sending the half message. It is thrown directly and will not be executed further throw new MQClientException("send message Exception", e); } ... }
The transaction message does not support delay. Before sending the message, two attributes are added to save the transaction message flag and producer group, and then sent to the broker as usual
3. The broker handles the half message org.apache.rocketmq.broker.processor.SendMessageProcessor#asyncSendMessageprivate CompletableFuture<RemotingCommand> asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request, SendMessageContext mqtraceContext, SendMessageRequestHeader requestHeader) { ... CompletableFuture<PutMessageResult> putMessageResult = null; // affair String transFlag = origProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED); if (transFlag != null && Boolean.parseBoolean(transFlag)) { // half message, prepared message of transaction // broker does not support transaction messages if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) { response.setCode(ResponseCode.NO_PERMISSION); response.setRemark( "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending transaction message is forbidden"); return CompletableFuture.completedFuture(response); } // Processing transaction messages putMessageResult = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner); } else { // Save message asynchronously putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner); } return handlePutMessageResultFuture(putMessageResult, response, request, msgInner, responseHeader, mqtraceContext, ctx, queueIdInt); }
org.apache.rocketmq.broker.transaction.queue.TransactionalMessageServiceImpl#asyncPrepareMessage
public CompletableFuture<PutMessageResult> asyncPrepareMessage(MessageExtBrokerInner messageInner) { return transactionalMessageBridge.asyncPutHalfMessage(messageInner); }
public CompletableFuture<PutMessageResult> asyncPutHalfMessage(MessageExtBrokerInner messageInner) { // Save the real topic and queueId of the message to the property // Convert transaction type to non physical type // Set topic to halfTopic // Set queueId to 0 // Then call store to store the message. return store.asyncPutMessage(parseHalfMessageInner(messageInner)); } private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) { MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic()); MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msgInner.getQueueId())); msgInner.setSysFlag( MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE)); msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic()); msgInner.setQueueId(0); msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); return msgInner; }
Here, after saving the original topic and queueId with attributes, set the topic to RMQ_SYS_TRANS_HALF_TOPIC and queueId are set to 0, and then sent to the messageStore for processing
However, when processing the half message, the commitlog will set the queueOffset to 0 and will not put the queueOffset into the topicqueueuetable.
public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank, final MessageExtBrokerInner msgInner, PutMessageContext putMessageContext) { ... // Transaction messages that require special handling // Transaction message final int tranType = MessageSysFlag.getTransactionValue(msgInner.getSysFlag()); switch (tranType) { // Prepared and Rollback message is not consumed, will not enter the // consumer queuec // Transaction preparation message, transaction cancellation message, and queueOffset is set to 0 case MessageSysFlag.TRANSACTION_PREPARED_TYPE: case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: queueOffset = 0L; break; case MessageSysFlag.TRANSACTION_NOT_TYPE: case MessageSysFlag.TRANSACTION_COMMIT_TYPE: default: break; } ... switch (tranType) { // The transaction message in the preparation phase and the transaction cancellation message are not written to the topicQueueTable, case MessageSysFlag.TRANSACTION_PREPARED_TYPE: case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: break; // Non transaction messages or transaction submission messages are placed in the topicqueueuetable case MessageSysFlag.TRANSACTION_NOT_TYPE: case MessageSysFlag.TRANSACTION_COMMIT_TYPE: // The next update ConsumeQueue information CommitLog.this.topicQueueTable.put(key, ++queueOffset); break; default: break; } return result; }
Then, the message sending result is returned to the producer
4. The producer executes a local transaction and returns the transaction execution resultLet's go back to the logic after the producer synchronously sends the half message and obtains the sending result
public TransactionSendResult sendMessageInTransaction(final Message msg, final LocalTransactionExecuter localTransactionExecuter, final Object arg) throws MQClientException { ... try { // Send messages synchronously and wait for the results. arg will not be sent to the server sendResult = this.send(msg); } catch (Exception e) { // An exception occurs when sending the half message. It is thrown directly and will not be executed further throw new MQClientException("send message Exception", e); } LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW; Throwable localException = null; switch (sendResult.getSendStatus()) { case SEND_OK: { // half msg was successfully sent to the broker try { if (sendResult.getTransactionId() != null) { msg.putUserProperty("__transactionId__", sendResult.getTransactionId()); } String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); if (null != transactionId && !"".equals(transactionId)) { msg.setTransactionId(transactionId); } if (null != localTransactionExecuter) { localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg); } else if (transactionListener != null) { log.debug("Used new transaction API"); // Start local transaction localTransactionState = transactionListener.executeLocalTransaction(msg, arg); } // After the local transaction is executed, it returns null if (null == localTransactionState) { localTransactionState = LocalTransactionState.UNKNOW; } // After the local transaction is executed, the submission message is not returned 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; // If disk brushing or slave synchronization fails, rollback is required and local transactions will not be executed case FLUSH_DISK_TIMEOUT: case FLUSH_SLAVE_TIMEOUT: case SLAVE_NOT_AVAILABLE: localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE; break; default: break; } try { // End the transaction and decide whether to commit or roll back the transaction according to the local transaction processing results this.endTransaction(msg, sendResult, localTransactionState, localException); } catch (Exception e) { log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e); } TransactionSendResult transactionSendResult = new TransactionSendResult(); transactionSendResult.setSendStatus(sendResult.getSendStatus()); transactionSendResult.setMessageQueue(sendResult.getMessageQueue()); transactionSendResult.setMsgId(sendResult.getMsgId()); transactionSendResult.setQueueOffset(sendResult.getQueueOffset()); transactionSendResult.setTransactionId(sendResult.getTransactionId()); transactionSendResult.setLocalTransactionState(localTransactionState); return transactionSendResult; }
When the half message fails to be sent, if the disk brushing fails and the synchronization slave fails, the transaction needs to be rolled back. localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
Only when the half message is sent successfully will the local transaction be executed
//Start local transaction localTransactionState = transactionListener.executeLocalTransaction(msg, arg);This is our own definition of listener
producer.setTransactionListener(new TransactionListener() { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { logger.info("Execute local transactions"); return LocalTransactionState.COMMIT_MESSAGE; } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { logger.info("Check local transactions"); return LocalTransactionState.COMMIT_MESSAGE; } });
After execution, return the corresponding LocalTransactionState,
Whether the half message is sent successfully or not, the local transaction is executed or not
try { //End the transaction and decide whether to commit or roll back the transaction according to the local transaction processing results this.endTransaction(msg, sendResult, localTransactionState, localException); } catch (Exception e) { log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e); }public void endTransaction( final Message msg, final SendResult sendResult, final LocalTransactionState localTransactionState, final Throwable localException) throws RemotingException, MQBrokerException, InterruptedException, UnknownHostException { final MessageId id; if (sendResult.getOffsetMsgId() != null) { id = MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId()); } else { id = MessageDecoder.decodeMessageId(sendResult.getMsgId()); } String transactionId = sendResult.getTransactionId(); final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName()); EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader(); requestHeader.setTransactionId(transactionId); requestHeader.setCommitLogOffset(id.getOffset()); switch (localTransactionState) { case COMMIT_MESSAGE: // Commit transaction requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE); break; case ROLLBACK_MESSAGE: // Rollback transaction requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE); break; case UNKNOW: // unknown requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE); break; default: break; } doExecuteEndTransactionHook(msg, sendResult.getMsgId(), brokerAddr, localTransactionState, false); requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); requestHeader.setTranStateTableOffset(sendResult.getQueueOffset()); requestHeader.setMsgId(sendResult.getMsgId()); String remark = localException != null ? ("executeLocalTransactionBranch exception: " + localException.toString()) : null; // Send single message this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark, this.defaultMQProducer.getSendMsgTimeout()); }
According to the local transaction execution result localTransactionState, determine the commitOrRollback of reqeustHeader
public void endTransactionOneway( final String addr, final EndTransactionRequestHeader requestHeader, final String remark, final long timeoutMillis ) throws RemotingException, MQBrokerException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.END_TRANSACTION, requestHeader); request.setRemark(remark); this.remotingClient.invokeOneway(addr, request, timeoutMillis); }
Encapsulate a requestcode.end_ The transaction message is sent to the broker in a one-way manner
5. The broker handles the local transaction results of the producerpublic void registerProcessor() { ... /** * EndTransactionProcessor */ this.remotingServer.registerProcessor(RequestCode.END_TRANSACTION, new EndTransactionProcessor(this), this.endTransactionExecutor); this.fastRemotingServer.registerProcessor(RequestCode.END_TRANSACTION, new EndTransactionProcessor(this), this.endTransactionExecutor); ... }
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(null); final EndTransactionRequestHeader requestHeader = (EndTransactionRequestHeader)request.decodeCommandCustomHeader(EndTransactionRequestHeader.class); LOGGER.debug("Transaction request:{}", requestHeader); // slave cannot process messages? 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; } // Results from transaction check if (requestHeader.getFromTransactionCheck()) { switch (requestHeader.getCommitOrRollback()) { // producer checked the transaction and returned an empty result case MessageSysFlag.TRANSACTION_NOT_TYPE: { LOGGER.warn("Check producer[{}] transaction state, but it's pending status." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), request.getRemark()); return null; } // Transaction commit case MessageSysFlag.TRANSACTION_COMMIT_TYPE: { LOGGER.warn("Check producer[{}] transaction state, the producer commit the message." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), request.getRemark()); break; } // Transaction rollback case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: { LOGGER.warn("Check producer[{}] transaction state, the producer rollback the message." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), request.getRemark()); break; } default: return null; } } else { switch (requestHeader.getCommitOrRollback()) { // Return is null, return is null 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; } // Commit transaction case MessageSysFlag.TRANSACTION_COMMIT_TYPE: { break; } // Rollback transaction case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: { LOGGER.warn("The producer[{}] end transaction in sending message, rollback the message." + "RequestHeader: {} Remark: {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()), requestHeader.toString(), request.getRemark()); break; } default: return null; } } OperationResult result = new OperationResult(); // Commit transaction if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) { // Read prepare message result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader); // Successfully read the complete message if (result.getResponseCode() == ResponseCode.SUCCESS) { // Check prepareMessage RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); if (res.getCode() == ResponseCode.SUCCESS) { // Construct msgInner according to the prepare message, then replace the topic with the original topic, clear the prepare flag and write to the commitlog 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()); MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED); // Store message in commitlog RemotingCommand sendResult = sendFinalMessage(msgInner); if (sendResult.getCode() == ResponseCode.SUCCESS) { // After successful writing, delete prepareMessage // Store the original preprocessed message into a new topic RMQ_SYS_TRANS_OP_HALF_TOPIC this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); } return sendResult; } return res; } } else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) { result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader); if (result.getResponseCode() == ResponseCode.SUCCESS) { RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); if (res.getCode() == ResponseCode.SUCCESS) { this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); } return res; } } response.setCode(result.getResponseCode()); response.setRemark(result.getResponseRemark()); return response; }
5.1 pending transactions
The producer's local transaction does not return commit/rollback. Maybe the transaction has not been completed, it will directly return null without any processing
5.2 commit
Read the complete half message, use the content of the half message, reconstruct a message, set its topic and queueId to the target value, and then deliver it to the commitlog, waiting for the reputService to be allocated to the corresponding consumeQueue, so that the consumer can consume the message, and then go through the logic of deleting the half message
five point three Rollback transaction
Go directly to the logic of deleting the half message
6. Delete the half message processDeleting the half message is actually modifying the half message to topic=RMQ_SYS_TRANS_OP_HALF_TOPIC, and then re post it to the commitlog
public boolean deletePrepareMessage(MessageExt msgExt) { if (this.transactionalMessageBridge.putOpMessage(msgExt, TransactionalMessageUtil.REMOVETAG)) { log.debug("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; } }
public boolean putOpMessage(MessageExt messageExt, String opType) { MessageQueue messageQueue = new MessageQueue(messageExt.getTopic(), this.brokerController.getBrokerConfig().getBrokerName(), messageExt.getQueueId()); if (TransactionalMessageUtil.REMOVETAG.equals(opType)) { return addRemoveTagInTransactionOp(messageExt, messageQueue); } return true; }
/** * Use this function while transaction msg is committed or rollback write a flag 'd' to operation queue for the * msg's offset * * @param messageExt Op message * @param messageQueue Op message queue * @return This method will always return true. */ private boolean addRemoveTagInTransactionOp(MessageExt messageExt, MessageQueue messageQueue) { Message message = new Message(TransactionalMessageUtil.buildOpTopic(), TransactionalMessageUtil.REMOVETAG, String.valueOf(messageExt.getQueueOffset()).getBytes(TransactionalMessageUtil.charset)); writeOp(message, messageQueue); return true; } private void writeOp(Message message, MessageQueue mq) { MessageQueue opQueue; if (opQueueMap.containsKey(mq)) { opQueue = opQueueMap.get(mq); } else { opQueue = getOpQueueByHalf(mq); MessageQueue oldQueue = opQueueMap.putIfAbsent(mq, opQueue); if (oldQueue != null) { opQueue = oldQueue; } } if (opQueue == null) { opQueue = new MessageQueue(TransactionalMessageUtil.buildOpTopic(), mq.getBrokerName(), mq.getQueueId()); } // Write message to commitlog putMessage(makeOpMessageInner(message, opQueue)); }7. Review the implementation results of the transaction
After the above normal system analysis, after the transaction is submitted, the consumer can consume the message; Transaction rollback, delete half message,
However, in a real scenario, messages such as half ACK / commit / rollback may be lost due to network jitter, but the half message is still on the broker; Or the transaction has not been executed for a long time, and the half message is also on the broker. At this time, you need to check whether the transaction has been executed or not, and whether the half message should commit or rollback
This is done by the transactional message service