RocketMQ——事务消息机制
一、事务消息概述
2018 年 07 月 24 日,RocketMQ 社区发布 4.3.0 版本,开始正式支持事务消息。
事务消息的实现方案目前分为2种:
-
- 两阶段提交方案
- 三阶段提交方案
RocketMQ 采取了两阶段提交的方案进行实现。
我们在说到事务时,通常会想到关系型数据库的事务,支持 ACID 四个特性。
-
- A,Atomicity,原子性。操作是一个不可分割的整体,要么都执行成功,要么都执行失败。
- C,Consistency,一致性。事务操作前后,数据必须是一致性的。
- I,Isolation,隔离性。多个事务同时执行时,不能互相干扰。
- D,Durability,持久性。一旦事务被提交,数据改变就是永久的,即使数据库宕机等都不会改变。
分布式事务是指在多个系统或多个数据库中的多个操作要么全部成功,要么全部失败,并且需要满足 ACID 四个特性。
二、事务消息机制
我们将事务消息的发送和处理总结为 4 个过程:
-
- 生产者发送事务消息
- 执行本地事务
- Broker回查事务消息
- Broker提交或者回滚事务消息
2.1 生产者发送事务消息和执行本地事务
事务消息的发送过程分为两个阶段:
-
- 第一阶段,发送事务消息
- 第二阶段,发送 endTransaction 消息
Broker 发送事务消息的过程,如下图所示:
事务发送过程的实现类 D: ocketmq-masterclientsrcmainjavaorgapache ocketmqclientproducerTransactionMQProducer.java,该类继承于 DefaultMQProducer,不仅能发送事务消息,还能发送其他消息。TransactionMQProducer的核心属性和方法如下:
transactionListener:事务监听器,主要功能是执行本地事务和执行事务回查。事务监听器包含 executeLocalTransaction() 和 checkLocalTransaction() 两个方法。executeLocalTransaction()方法执行本地事务,checkLocalTransaction()方法是当生产者由于各种问题导致未发送 Commit 或 Rollback 消息给 Broker 时,Broker 回调生产者查询本地事务状态的处理方法。
executorService:Broker 回查请求处理的线程池。
start():事务消息生产者的启动方法,与普通启动方法不同,增加了 this.defaultMQProducerImpl.initTransactionEnv() 的调用,即增加了初始化事务消息的环境信息。代码路径:D: ocketmq-masterclientsrcmainjavaorgapache ocketmqclientimplproducerDefaultMQProducerImpl.java,具体代码如下:
1 public void initTransactionEnv() { 2 TransactionMQProducer producer = (TransactionMQProducer) this.defaultMQProducer; 3 if (producer.getExecutorService() != null) { 4 this.checkExecutor = producer.getExecutorService(); 5 } else { 6 this.checkRequestQueue = new LinkedBlockingQueue<Runnable>(producer.getCheckRequestHoldMax()); 7 this.checkExecutor = new ThreadPoolExecutor( 8 producer.getCheckThreadPoolMinSize(), 9 producer.getCheckThreadPoolMaxSize(), 10 1000 * 60, 11 TimeUnit.MILLISECONDS, 12 this.checkRequestQueue); 13 } 14 }
从上面看,事务消息的环境初始化主要用于初始化 Broker 回查请求处理的线程池,在初始化事务消息生产者时,我们可以指定初始化对象,如果不指定初始化对象,那么这里会初始化一个单线程的线程池。
shutdown():关闭生产者,回收生产者资源。该方法是启动方法的逆过程,功能是关闭生产者、销毁事务环境。销毁事务环境是指销毁事务回查线程池,清除回查任务队列。
生产者发送事务消息,主要分为以下两个阶段:
(1)发送 Half 消息的过程。
-
- 第一步:数据校验。
- 第二步:消息预处理。
- 第三步:发送事务消息。
(2)发送 Commit 或 Rollback 消息。
当前 Half 消息发送完成后,会返回生产者消息发送到哪个 Broker、消息位点是多少,再根据本地事务的执行结果封装 EndTransactionReuqestHeader 对象,最后调用 MQClientAPIImpl.endTransactionOneway()方法通知 Broker 进行 Commit 或 Rollback。
2.2 Broker 存储事务消息
事务消息的初始化是通过 D: ocketmq-masterrokersrcmainjavaorgapache ocketmqrokerBrokerController.java 中 initialTransaction() 方法执行的,事务消息处理的初始化代码如下:
1 private void initialTransaction() { 2 this.transactionalMessageService = ServiceProvider.loadClass(ServiceProvider.TRANSACTION_SERVICE_ID, TransactionalMessageService.class); 3 if (null == this.transactionalMessageService) { 4 this.transactionalMessageService = new TransactionalMessageServiceImpl(new TransactionalMessageBridge(this, this.getMessageStore())); 5 log.warn("Load default transaction message hook service: {}", TransactionalMessageServiceImpl.class.getSimpleName()); 6 } 7 this.transactionalMessageCheckListener = ServiceProvider.loadClass(ServiceProvider.TRANSACTION_LISTENER_ID, AbstractTransactionalMessageCheckListener.class); 8 if (null == this.transactionalMessageCheckListener) { 9 this.transactionalMessageCheckListener = new DefaultTransactionalMessageCheckListener(); 10 log.warn("Load default discard message hook service: {}", DefaultTransactionalMessageCheckListener.class.getSimpleName()); 11 } 12 this.transactionalMessageCheckListener.setBrokerController(this); 13 this.transactionalMessageCheckService = new TransactionalMessageCheckService(this); 14 }
(1) transactionalMessageService
事务消息主要用于处理服务,默认实现类是 TransactionalMessageServiceImpl。如果想自定义事务消息处理实现类,需要实现 TransactionalMessageService 接口,然后通过 ServiceProvider.loadClass() 方法进行加载。TransactionalMessageService 接入是如何定义事务的基本操作的呢?代码路径:D: ocketmq-masterrokersrcmainjavaorgapache ocketmqroker ransactionTransactionalMessageService.java,具体代码如下:
1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 package org.apache.rocketmq.broker.transaction; 18 19 import org.apache.rocketmq.common.message.MessageExt; 20 import org.apache.rocketmq.common.protocol.header.EndTransactionRequestHeader; 21 import org.apache.rocketmq.store.MessageExtBrokerInner; 22 import org.apache.rocketmq.store.PutMessageResult; 23 import java.util.concurrent.CompletableFuture; 24 25 public interface TransactionalMessageService { 26 27 /** 28 * Process prepare message, in common, we should put this message to storage service. 29 * 30 * @param messageInner Prepare(Half) message. 31 * @return Prepare message storage result. 32 */ 33 PutMessageResult prepareMessage(MessageExtBrokerInner messageInner); #用于保存 Half 事务消息,用户可以对其进行 Commit 或 Rollback。 34 35 /** 36 * Process prepare message in async manner, we should put this message to storage service 37 * 38 * @param messageInner Prepare(Half) message. 39 * @return CompletableFuture of put result, will be completed at put success(flush and replica done) 40 */ 41 CompletableFuture<PutMessageResult> asyncPrepareMessage(MessageExtBrokerInner messageInner); 42 43 /** 44 * Delete prepare message when this message has been committed or rolled back. 45 * 46 * @param messageExt 47 */ 48 boolean deletePrepareMessage(MessageExt messageExt); #用于删除事务消息,一般用于 Broker 回查失败的 Half 消息。 49 50 /** 51 * Invoked to process commit prepare message. 52 * 53 * @param requestHeader Commit message request header. 54 * @return Operate result contains prepare message and relative error code. 55 */ 56 OperationResult commitMessage(EndTransactionRequestHeader requestHeader); #用于提交事务消息,使消费者可以正常地消费事务消息。 57 58 /** 59 * Invoked to roll back prepare message. 60 * 61 * @param requestHeader Prepare message request header. 62 * @return Operate result contains prepare message and relative error code. 63 */ 64 OperationResult rollbackMessage(EndTransactionRequestHeader requestHeader); #用于回滚事务消息,回滚后消费者将不能够消费该消息。通常用于生产者主动进行 Rollback 时,以及 Broker 回查的生产者本地事务失败时。 65 66 /** 67 * Traverse uncommitted/unroll back half message and send check back request to producer to obtain transaction 68 * status. 69 * 70 * @param transactionTimeout The minimum time of the transactional message to be checked firstly, one message only 71 * exceed this time interval that can be checked. 72 * @param transactionCheckMax The maximum number of times the message was checked, if exceed this value, this 73 * message will be discarded. 74 * @param listener When the message is considered to be checked or discarded, the relative method of this class will 75 * be invoked. 76 */ 77 void check(long transactionTimeout, int transactionCheckMax, AbstractTransactionalMessageCheckListener listener); 78 79 /** 80 * Open transaction service. 81 * 82 * @return If open success, return true. 83 */ 84 boolean open(); #用于打开事务服务 85 86 /** 87 * Close transaction service. 88 */ 89 void close(); #用于关闭事务服务 90 }
(2) transactionalMessageService
事务消息回查监听器,默认实现类是 DefaultTransactionalMessageCheckListener。如果想自定义回查监听处理,需要继承 AbstractTransactionalMessageCheckListener 接口,然后通过 ServiceProvider.loadClass()方法被加载。
(3) transactionalMessageCheckService
事务消息回查服务是一个线程服务,定时调用 transactionalMessageService.check() 方法,检查超时的 Half 消息是否需要回查。
上面三个事务处理完成初始化后,Broker 就可以处理事务消息了。
Broker 存储事务消息和普通消息都是通过 org.apache.rocketmq.broker.processor.SendMessageProcessor 类进行处理的,只是在存储消息时有两处事务消息需要单独处理。
第一个单独处理:判断是否是事务消息,处理方法的代码路径:D: ocketmq-masterrokersrcmainjavaorgapache ocketmqrokerprocessorSendMessageProcessor.java 中 sendMessage(),具体代码如下:
1 private RemotingCommand sendMessage(final ChannelHandlerContext ctx, 2 final RemotingCommand request, 3 final SendMessageContext sendMessageContext, 4 final SendMessageRequestHeader requestHeader) throws RemotingCommandException { 5 6 final RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class); 7 final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader)response.readCustomHeader(); 8 9 response.setOpaque(request.getOpaque()); 10 11 response.addExtField(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId()); 12 response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(this.brokerController.getBrokerConfig().isTraceOn())); 13 14 log.debug("receive SendMessage request command, {}", request); 15 16 final long startTimstamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp(); 17 if (this.brokerController.getMessageStore().now() < startTimstamp) { 18 response.setCode(ResponseCode.SYSTEM_ERROR); 19 response.setRemark(String.format("broker unable to service, until %s", UtilAll.timeMillisToHumanString2(startTimstamp))); 20 return response; 21 } 22 23 response.setCode(-1); 24 super.msgCheck(ctx, requestHeader, response); 25 if (response.getCode() != -1) { 26 return response; 27 } 28 29 final byte[] body = request.getBody(); 30 31 int queueIdInt = requestHeader.getQueueId(); 32 TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); 33 34 if (queueIdInt < 0) { 35 queueIdInt = Math.abs(this.random.nextInt() % 99999999) % topicConfig.getWriteQueueNums(); 36 } 37 38 MessageExtBrokerInner msgInner = new MessageExtBrokerInner(); 39 msgInner.setTopic(requestHeader.getTopic()); 40 msgInner.setQueueId(queueIdInt); 41 42 if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig)) { 43 return response; 44 } 45 46 msgInner.setBody(body); 47 msgInner.setFlag(requestHeader.getFlag()); 48 MessageAccessor.setProperties(msgInner, MessageDecoder.string2messageProperties(requestHeader.getProperties())); 49 msgInner.setBornTimestamp(requestHeader.getBornTimestamp()); 50 msgInner.setBornHost(ctx.channel().remoteAddress()); 51 msgInner.setStoreHost(this.getStoreHost()); 52 msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes()); 53 String clusterName = this.brokerController.getBrokerConfig().getBrokerClusterName(); 54 MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_CLUSTER, clusterName); 55 msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); 56 PutMessageResult putMessageResult = null; 57 Map<String, String> oriProps = MessageDecoder.string2messageProperties(requestHeader.getProperties()); 58 String traFlag = oriProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED); #如果该值为 True 则当前消息是事务消息。 59 if (traFlag != null && Boolean.parseBoolean(traFlag) 60 && !(msgInner.getReconsumeTimes() > 0 && msgInner.getDelayTimeLevel() > 0)) { //For client under version 4.6.1 61 if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) { #判断当前 Broker 师傅支持事务消息。 62 response.setCode(ResponseCode.NO_PERMISSION); 63 response.setRemark( 64 "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() 65 + "] sending transaction message is forbidden"); 66 return response; 67 } 68 putMessageResult = this.brokerController.getTransactionalMessageService().prepareMessage(msgInner); #调用TransactionalMessageService.prepareMessage()方法保存 Half 消息。 69 } else { 70 putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); 71 } 72 73 return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt); 74 75 }
第二个单独处理:存储前事务消息预处理,代码路径:D: ocketmq-masterrokersrcmainjavaorgapache ocketmqroker ransactionqueueTransactionalMessageBridge.java,执行 parseHalfMessageInner() 方法,具体代码如下:
1 private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) { 2 MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic()); 3 MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID, 4 String.valueOf(msgInner.getQueueId())); 5 msgInner.setSysFlag( 6 MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE)); 7 msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic()); 8 msgInner.setQueueId(0); 9 msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties())); 10 return msgInner; 11 }
以上代码的功能是将原消息的 Topic、queueId、sysFlg 存储在消息的扩展字段中,并且修改 Topic 的值为 RMQ_SYS_TRANS_HALF_TOPIC,修改 queueId 的值为 0。然后,与其他消息一样,调用 DefaultMessageStore.putMessage()方法保存到 CommitLog 中。
CommitLog 存储成功后,通过 org.apache.rocketmq.store.CommitLog.DefaultAppendMessageCallback.doAppend() 方法单独对事务消息进行处理,代码路径:D: ocketmq-masterstoresrcmainjavaorgapache ocketmqstoreCommitLog.java,具体代码如下:
1 public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank, 2 final MessageExtBrokerInner msgInner) { 3 // STORETIMESTAMP + STOREHOSTADDRESS + OFFSET <br> 4 5 // PHY OFFSET 6 long wroteOffset = fileFromOffset + byteBuffer.position(); 7 8 int sysflag = msgInner.getSysFlag(); 9 10 int bornHostLength = (sysflag & MessageSysFlag.BORNHOST_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; 11 int storeHostLength = (sysflag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0 ? 4 + 4 : 16 + 4; 12 ByteBuffer bornHostHolder = ByteBuffer.allocate(bornHostLength); 13 ByteBuffer storeHostHolder = ByteBuffer.allocate(storeHostLength); 14 15 this.resetByteBuffer(storeHostHolder, storeHostLength); 16 String msgId; 17 if ((sysflag & MessageSysFlag.STOREHOSTADDRESS_V6_FLAG) == 0) { 18 msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(storeHostHolder), wroteOffset); 19 } else { 20 msgId = MessageDecoder.createMessageId(this.msgIdV6Memory, msgInner.getStoreHostBytes(storeHostHolder), wroteOffset); 21 } 22 23 // Record ConsumeQueue information 24 keyBuilder.setLength(0); 25 keyBuilder.append(msgInner.getTopic()); 26 keyBuilder.append('-'); 27 keyBuilder.append(msgInner.getQueueId()); 28 String key = keyBuilder.toString(); 29 Long queueOffset = CommitLog.this.topicQueueTable.get(key); 30 if (null == queueOffset) { 31 queueOffset = 0L; 32 CommitLog.this.topicQueueTable.put(key, queueOffset); 33 } 34 35 // Transaction messages that require special handling 36 final int tranType = MessageSysFlag.getTransactionValue(msgInner.getSysFlag()); 37 switch (tranType) { 38 // Prepared and Rollback message is not consumed, will not enter the 39 // consumer queuec 40 case MessageSysFlag.TRANSACTION_PREPARED_TYPE: 41 case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: 42 queueOffset = 0L; 43 break; 44 case MessageSysFlag.TRANSACTION_NOT_TYPE: 45 case MessageSysFlag.TRANSACTION_COMMIT_TYPE: 46 default: 47 break; 48 } 49 50 /** 51 * Serialize message 52 */ 53 final byte[] propertiesData = 54 msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8); 55 56 final int propertiesLength = propertiesData == null ? 0 : propertiesData.length; 57 58 if (propertiesLength > Short.MAX_VALUE) { 59 log.warn("putMessage message properties length too long. length={}", propertiesData.length); 60 return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED); 61 } 62 63 final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8); 64 final int topicLength = topicData.length; 65 66 final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length; 67 68 final int msgLen = calMsgLength(msgInner.getSysFlag(), bodyLength, topicLength, propertiesLength); 69 70 // Exceeds the maximum message 71 if (msgLen > this.maxMessageSize) { 72 CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength 73 + ", maxMessageSize: " + this.maxMessageSize); 74 return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED); 75 } 76 77 // Determines whether there is sufficient free space 78 if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) { 79 this.resetByteBuffer(this.msgStoreItemMemory, maxBlank); 80 // 1 TOTALSIZE 81 this.msgStoreItemMemory.putInt(maxBlank); 82 // 2 MAGICCODE 83 this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE); 84 // 3 The remaining space may be any value 85 // Here the length of the specially set maxBlank 86 final long beginTimeMills = CommitLog.this.defaultMessageStore.now(); 87 byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank); 88 return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(), 89 queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills); 90 } 91 92 // Initialization of storage space 93 this.resetByteBuffer(msgStoreItemMemory, msgLen); 94 // 1 TOTALSIZE 95 this.msgStoreItemMemory.putInt(msgLen); 96 // 2 MAGICCODE 97 this.msgStoreItemMemory.putInt(CommitLog.MESSAGE_MAGIC_CODE); 98 // 3 BODYCRC 99 this.msgStoreItemMemory.putInt(msgInner.getBodyCRC()); 100 // 4 QUEUEID 101 this.msgStoreItemMemory.putInt(msgInner.getQueueId()); 102 // 5 FLAG 103 this.msgStoreItemMemory.putInt(msgInner.getFlag()); 104 // 6 QUEUEOFFSET 105 this.msgStoreItemMemory.putLong(queueOffset); 106 // 7 PHYSICALOFFSET 107 this.msgStoreItemMemory.putLong(fileFromOffset + byteBuffer.position()); 108 // 8 SYSFLAG 109 this.msgStoreItemMemory.putInt(msgInner.getSysFlag()); 110 // 9 BORNTIMESTAMP 111 this.msgStoreItemMemory.putLong(msgInner.getBornTimestamp()); 112 // 10 BORNHOST 113 this.resetByteBuffer(bornHostHolder, bornHostLength); 114 this.msgStoreItemMemory.put(msgInner.getBornHostBytes(bornHostHolder)); 115 // 11 STORETIMESTAMP 116 this.msgStoreItemMemory.putLong(msgInner.getStoreTimestamp()); 117 // 12 STOREHOSTADDRESS 118 this.resetByteBuffer(storeHostHolder, storeHostLength); 119 this.msgStoreItemMemory.put(msgInner.getStoreHostBytes(storeHostHolder)); 120 // 13 RECONSUMETIMES 121 this.msgStoreItemMemory.putInt(msgInner.getReconsumeTimes()); 122 // 14 Prepared Transaction Offset 123 this.msgStoreItemMemory.putLong(msgInner.getPreparedTransactionOffset()); 124 // 15 BODY 125 this.msgStoreItemMemory.putInt(bodyLength); 126 if (bodyLength > 0) 127 this.msgStoreItemMemory.put(msgInner.getBody()); 128 // 16 TOPIC 129 this.msgStoreItemMemory.put((byte) topicLength); 130 this.msgStoreItemMemory.put(topicData); 131 // 17 PROPERTIES 132 this.msgStoreItemMemory.putShort((short) propertiesLength); 133 if (propertiesLength > 0) 134 this.msgStoreItemMemory.put(propertiesData); 135 136 final long beginTimeMills = CommitLog.this.defaultMessageStore.now(); 137 // Write messages to the queue buffer 138 byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen); 139 140 AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId, 141 msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills); 142 143 switch (tranType) { 144 case MessageSysFlag.TRANSACTION_PREPARED_TYPE: 145 case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE: 146 break; 147 case MessageSysFlag.TRANSACTION_NOT_TYPE: 148 case MessageSysFlag.TRANSACTION_COMMIT_TYPE: 149 // The next update ConsumeQueue information 150 CommitLog.this.topicQueueTable.put(key, ++queueOffset); 151 break; 152 default: 153 break; 154 } 155 return result; 156 }
Prepared 消息其实就是 Half 消息,其实现逻辑是,设置当前 Half 消息的 queueOffset 值为 0,而不是其真实的位点值。这样,该位点就不会建立 Consume Queue 索引,自然也不能被消费者消费。
2.3 Broker 回查事务消息
如果用于由于某种原因,在第二阶段中没有将 endTransaction 消息发送给 Broker,那么 Broker 的 Half 消息要怎么处理呢?
RocketMQ 在设计时已经考虑到这个问题,通过“回查机制”处理第二阶段既未发送 Commit 也没有发送 Rollback 的消息。回查是 Broker 发起的,Broker 认为在接收 Half 消息后的一段时间内,如果生产者都没有发送 Commit 或 Rollback 消息给 Broker,那么 Broker 会主动 "询问"生产者该事务消息对应的本地事务执行结果,以此来决定事务是否要 Commit。
2.4 Broker 提交或回滚事务消息
当生产者本地事务处理完成并且 Broker 回查事务消息后,不管执行 Commit 还是 Rollback,都会根据用户本地事务的执行结果发送一个 end_transaction 的 RPC 请求给 Broker,Broker 端处理该请求的类是 D: ocketmq-masterrokersrcmainjavaorgapache ocketmqrokerprocessorEndTransactionProcessor.java,其核心处理步骤如下:
第一步:end_transaction 请求校验。主要检查项如下:
-
- Broker 角色检查。Slave Broker 不处理事务消息。
- 事务消息类型检查。EndTransactionProcessor 只处理 Commit 或 Rollback 类型的事务消息,其余消息都不处理。
第二步:进行 Commit 或 Rollback。根据生产者请求头中的参数判断,是 Commit 请求还是 Rollback 请求,然后分别进行处理。 1 if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) { 2 result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader); #提交 Half 消息。
3 if (result.getResponseCode() == ResponseCode.SUCCESS) { 4 RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); #Half消息数据校验。校验内容包含发送消息的生产者组与当前执行 Commit/Rollack 的生产者是否一致,当前Half消息是否与请求Commit/Rollback的消息是同一条消息。 5 if (res.getCode() == ResponseCode.SUCCESS) { 6 MessageExtBrokerInner msgInner = endMessageTransaction(result.getPrepareMessage()); #消息对象类型转化,将 MessageExt 对象转化为 MessageExtBrokerInner 对象,并且还原消息之前的 Topic 和 Consume Queue 等信息。 7 msgInner.setSysFlag(MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), requestHeader.getCommitOrRollback())); 8 msgInner.setQueueOffset(requestHeader.getTranStateTableOffset()); 9 msgInner.setPreparedTransactionOffset(requestHeader.getCommitLogOffset()); 10 msgInner.setStoreTimestamp(result.getPrepareMessage().getStoreTimestamp()); 11 MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_TRANSACTION_PREPARED); 12 RemotingCommand sendResult = sendFinalMessage(msgInner); #将还原后的事务消息最终发送到 CommitLog 中。一旦发送成功,消费者就可以正常拉取消息并消费。 13 if (sendResult.getCode() == ResponseCode.SUCCESS) { 14 this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); #在 sendFinalMessage() 执行成功后,删除 Half 消息。其实 RocketMQ 是不能真正删除消息的,其实质是顺序写磁盘,
相当于做了一个“假删除”。“假删除”通过 putOpMessage() 方法将消息保存到 TransactionalMessageUtil.buildOpTopic()的 Topic 中,并且做上标记 TransactionalMessageUtil.REMOVETAG,表示消息已删除。 15 } 16 return sendResult; 17 } 18 return res; 19 } 20 } else if (MessageSysFlag.TRANSACTION_ROLLBACK_TYPE == requestHeader.getCommitOrRollback()) { 21 result = this.brokerController.getTransactionalMessageService().rollbackMessage(requestHeader); 22 if (result.getResponseCode() == ResponseCode.SUCCESS) { 23 RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); 24 if (res.getCode() == ResponseCode.SUCCESS) { 25 this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); 26 } 27 return res; 28 } 29 }
保存 OP 消息,代码路径:D: ocketmq-masterrokersrcmainjavaorgapache ocketmqroker ransactionqueueTransactionalMessageBridge.java,具体代码如下:
1 public boolean putOpMessage(MessageExt messageExt, String opType) { 2 MessageQueue messageQueue = new MessageQueue(messageExt.getTopic(), 3 this.brokerController.getBrokerConfig().getBrokerName(), messageExt.getQueueId()); 4 if (TransactionalMessageUtil.REMOVETAG.equals(opType)) { 5 return addRemoveTagInTransactionOp(messageExt, messageQueue); 6 } 7 return true; 8 }
如果消息被标记为已删除,则调用 addRemoveTagInTransactionOp() 方法,利用标记为已删除的 OP 消息构造 Message 消息对象,并且调用存储方法保存消息。代码路径:D: ocketmq-masterrokersrcmainjavaorgapache ocketmqroker ransactionqueueTransactionalMessageBridge.java,具体代码如下:
1 private boolean addRemoveTagInTransactionOp(MessageExt messageExt, MessageQueue messageQueue) { 2 Message message = new Message(TransactionalMessageUtil.buildOpTopic(), TransactionalMessageUtil.REMOVETAG, 3 String.valueOf(messageExt.getQueueOffset()).getBytes(TransactionalMessageUtil.charset)); 4 writeOp(message, messageQueue); 5 return true; 6 }