如何使用
-
直接看官方案例
/rocketmq-example/src/main/java/org/apache/rocketmq/example/transaction
-
源码
inipublic static void main(String[] args) throws MQClientException, InterruptedException { //K1 创建TransactionListener 实例,字面理解为事务消息事件监听器 TransactionListener transactionListener = new TransactionListenerImpl(); TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); //K1 ExecutorService executorService,创建一个线程池,其线程的名称前缀"client-transaction-msg-check-thread", // 从字面理解为客户端事务消息状态检测线程,我们可以大胆的猜测一下是不是这个线程池调用TransactionListener方法,完成对事务消息的检测呢? // 【这里只是作者的猜测,大家不能当真,在作者后续文章发布后,如果该观点错误,会加以修复,这里写出来,主要是想分享一下我读源码的方法】 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; } }); //K1 为事务消息发送者设置线程池。 producer.setExecutorService(executorService); //K1 为事务消息发送者设置事务监听器 producer.setTransactionListener(transactionListener); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; for (int i = 0; i < 10; i++) { try { Message msg = new Message("TopicTest1234", tags[i % tags.length], "KEY" + i, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.sendMessageInTransaction(msg, null); System.out.printf("%s%n", sendResult); Thread.sleep(10); } catch (MQClientException | UnsupportedEncodingException e) { e.printStackTrace(); } } for (int i = 0; i < 100000; i++) { Thread.sleep(1000); } producer.shutdown(); }
-
接下来看看监听器的源码
javapublic class TransactionListenerImpl implements TransactionListener { private AtomicInteger transactionIndex = new AtomicInteger(0); private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>(); @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { int value = transactionIndex.getAndIncrement(); int status = value % 3; localTrans.put(msg.getTransactionId(), status); return LocalTransactionState.UNKNOW; } /** * 记录本地事务的事务状态 */ @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { Integer status = localTrans.get(msg.getTransactionId()); if (null != status) { switch (status) { case 0: return LocalTransactionState.UNKNOW; case 1: return LocalTransactionState.COMMIT_MESSAGE; case 2: return LocalTransactionState.ROLLBACK_MESSAGE; default: return LocalTransactionState.COMMIT_MESSAGE; } } return LocalTransactionState.COMMIT_MESSAGE; } }
源码分析
第一次提交
生产端
- 从上面的代码可以看出,入口在 producer.sendMessageInTransaction
DefaultMQProducerImpl
sendMessageInTransaction
-
基本参数校验,忽略延迟级别,是不是可以说事务消息和延迟消息不共存呢???后面看看具体Broker端的逻辑,此处暂且这么认为
csharp//K1 必要参数校验 if (null == localTransactionExecuter && null == transactionListener) { throw new MQClientException("tranExecutor is null", null); } //IMP 延迟级别不生效 // ignore DelayTimeLevel parameter if (msg.getDelayTimeLevel() != 0) { MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL); }
-
设置事务消息特有的标记,这个见多不怪了,不管是顺序消息,还是重试消息,都会在Properties中记录一些额外的属性信息,这个方法挺好的。还有访问的工具类,可以学习一波,用于Map中常量的设置
kotlin//IMP 事务消息的标志 MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); //IMP 设置生产者组 MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
-
发送事务消息
csharptry { //K1 同步发送消息 sendResult = this.send(msg); } catch (Exception e) { throw new MQClientException("send message Exception", e); }
-
判断发送状态,如果发送成功的话,执行本地事务方法,此处有几个疑问的地方
- 为啥存储了两次transactionId,而且还是不同地方获取,不同地方存储,什么情况,难道是新老版本兼容的问题
- 只有发送成功,还有一些超时错误,如果发送失败呢,难道发送失败就是UNKNOW吗
iniLocalTransactionState localTransactionState = LocalTransactionState.UNKNOW; Throwable localException = null; switch (sendResult.getSendStatus()) { case SEND_OK: { 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); } //K1 执行本地事务方法 if (null != localTransactionExecuter) { localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg); } else if (transactionListener != null) { log.debug("Used new transaction API"); localTransactionState = transactionListener.executeLocalTransaction(msg, arg); } 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; }
-
根据发送状态,执行后续的逻辑处理,感觉应该有重试机制,还有本地事务执行成功,失败,对应的半消息的提交和回滚操作,暂且想想,后续细看
ini//K1 执行事务预提交之后的逻辑 try { 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;
上面的send方法最后会走到通用的 sendDefaultImpl方法中,主要观察有关事务的内容
sendKernelImpl
-
发送消息之前,判断是否是事务消息,如果是的话,设置一个标记位,表示此刻的消息是事务消息,需要特殊处理
ini//事务消息标志,prepare消息 final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED); if (Boolean.parseBoolean(tranMsg)) { sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE; }
最后会走到 MQClientAPIImpl 的方法中进行发送消息,此处的逻辑按照设计肯定是一致的,只是消息的一些标志位为事务消息
sendMessage
-
构造了事务消息的recommandCode为SEND_MESSAGE
inirequest = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
-
根据不同的方式发送消息,事务消息,在一开始发送消息的时候就设置好了SYNC,因为篇幅有限,所以简化掉了,可以去源码中查看
arduinocase SYNC: /* * 发送消息之前,进行超时检查,如果已经超时了那么取消本次发送操作,抛出异常 */ long costTimeSync = System.currentTimeMillis() - beginStartTime; if (timeout < costTimeSync) { throw new RemotingTooMuchRequestException("sendKernelImpl call timeout"); } /* * K2 10 发送单向、同步消息 */ sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, mq.getBrokerName(), msg, requestHeader, timeout - costTimeSync, communicationMode, context, this); break;
-
各种重载之后,最后落到了下面的方法中,此刻的request与普通消息有什么不同呢
- request的RequestHeader中的sysFlag存在着事务消息标志位
- request的RequestHeader中的存在着事务消息标志位,还有生产者组信息
javaprivate SendResult sendMessageSync( final String addr, final String brokerName, final Message msg, final long timeoutMillis, final RemotingCommand request ) throws RemotingException, MQBrokerException, InterruptedException { /* * K2 发送同步消息,request中会存储消息体信息 */ RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis); assert response != null; /* * K2 处理响应结果 */ return this.processSendResponse(brokerName, msg, response, addr); }
scssSendMessageRequestHeader requestHeader = new SendMessageRequestHeader(); requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); requestHeader.setTopic(msg.getTopic()); requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey()); requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums()); requestHeader.setQueueId(mq.getQueueId()); //包含了事务消息 requestHeader.setSysFlag(sysFlag); requestHeader.setBornTimestamp(System.currentTimeMillis()); requestHeader.setFlag(msg.getFlag()); //msg的所有属性都会给到requestHeader requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties())); requestHeader.setReconsumeTimes(0); requestHeader.setUnitMode(this.isUnitMode()); requestHeader.setBatch(msg instanceof MessageBatch);
发送端,就是和普通消息没什么区别,就是加入了标志位,recommandCode也是正常的发送消息的Code
Broker端
根据recommandCode找到处理消息下的处理类
SendMessageProcessor
asyncSendMessage
- 获取到事务消息标记位,如果当前的Broker没有权限 的话,直接返回没有权限执行,第一种错误,等会看看这些错误Producer收到之后是怎么处理的,所有的返回用蓝色来表示
kotlin
String transFlag = origProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (transFlag != null && Boolean.parseBoolean(transFlag)) {
//判断是否需要拒绝事务消息,如果需要拒绝,则返回NO_PERMISSION异常
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);
}
//调用asyncPrepareMessage方法以异步的方式处理、存储事务准备消息,底层仍是asyncPutMessage方法
putMessageResult = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner);
}
上面的 asyncPrepareMessage,不断地去重载调用,下面只针对方法,没有写出类
typescript
@Override
public CompletableFuture<PutMessageResult> asyncPrepareMessage(MessageExtBrokerInner messageInner) {
return transactionalMessageBridge.asyncPutHalfMessage(messageInner);
}
typescript
public CompletableFuture<PutMessageResult> asyncPutHalfMessage(MessageExtBrokerInner messageInner) {
return store.asyncPutMessage(parseHalfMessageInner(messageInner));
}
parseHalfMessageInner,什么意思,主要针对事务消息,进行进一步封装,啥意思呢,不知道记不记得之前学习的重试消息之类的,MQ这边充分利用的主题和队列的概念,如果队列变化,主题需要进行重置,以及Properties中隐藏真实的主题,此处也是这样,具体操作呢:备份消息的原主题名称与原队列ID,然后取消是事务消息的消息标签,重新设置消息的主题为:RMQ_SYS_TRANS_HALF_TOPIC,队列ID固定为0
less
private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
//K1 备份原主题
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
//K1 备份原队列ID
MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
String.valueOf(msgInner.getQueueId()));
//K1 取消事务标签,怎么说呢,因为这个东西就是判断失误消息的,现在没用了,而且变成了新的主题消息,那么就没用了
msgInner.setSysFlag(
MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
//K1 构造半消息主题,常量
msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
//K1 队列为0
msgInner.setQueueId(0);
msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
return msgInner;
}
最后调用 DefaultMessageStore 的store方法进行存储消息,现在把事务消息存储到了半消息主题中。回到上面 sendMessageInTransaction方法的第四步,针对事务消息发送之后的状态进行处理
第一次提交之后,本地处理
生产端
DefaultMQProducerImpl
sendMessageInTransaction
- 注意:TransactionListener#executeLocalTransaction是在发送者成功发送PREPARED消息后,会执行本地事务方法,然后返回本地事务状态;如果PREPARED消息发送失败,则不会调用TransactionListener#executeLocalTransaction,并且本地事务消息,设置为LocalTransactionState.ROLLBACK_MESSAGE,表示消息需要被回滚。
ini
//本地事务状态
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable localException = null;
switch (sendResult.getSendStatus()) {
case SEND_OK: {
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);
}
//K1 执行本地事务方法
if (null != localTransactionExecuter) {
//IMP 执行本地业务方法,需要用户自定义去实现
localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
} else if (transactionListener != null) {
log.debug("Used new transaction API");
localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
}
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;
}
- 根据远程消息和本地事务的执行状态,获取到执行状态,判断当前的事务消息是需要提交还是回滚
ini
//K1 执行事务预提交之后的逻辑
try {
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;
endTransaction
-
根据不同的事务执行情况,构造出事务请求,结束此次事务
- COMMIT_MESSAGE :提交事务。
- ROLLBACK_MESSAGE:回滚事务。
- UNKNOW:未知事务状态
javapublic 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()); } //K2 事务ID String transactionId = sendResult.getTransactionId(); final String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName()); EndTransactionRequestHeader requestHeader = new EndTransactionRequestHeader(); requestHeader.setTransactionId(transactionId); requestHeader.setCommitLogOffset(id.getOffset()); //K2 根据执行状态,设置消息的标记位 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; } 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; this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, requestHeader, remark, this.defaultMQProducer.getSendMsgTimeout()); }
endTransactionOneway
- 会看到重新构造了消息体,但是此处并没有设置主题信息啥的,为啥呢???因为办消息主题就一个,而且队列也一个,所以此处只有msId,标记位之类的好吧,这也说明了,事务消息全权交给Broker进行处理,,此处的RequestCommand为 END_TRANSACTION ,此刻要注意是OneWay,啥意思,不看返回,可能会丢失这次请求
arduinopublic 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); }
第二次提交
根据requestCommand,可以找到处理该请求的入口在 EndTransactionProcessor
Broker端
EndTransactionProcessor
processRequest
- 核心实现就是根据commitlogOffset找到消息,如果是提交动作,就恢复原消息的主题与队列,再次存入commitlog文件进而转到消息消费队列,供消费者消费,然后将原预处理消息存入一个新的主题RMQ_SYS_TRANS_OP_HALF_TOPIC,代表该消息已被处理;回滚消息与提交事务消息不同的是,提交事务消息会将消息恢复原主题与队列,再次存储在commitlog文件中。回滚消息,不会恢复原主题,放入到 commitLog 中
-
针对,事务消息的 提交 和 回滚进行了处理,但是针对未知状态呢,不进行任何操作,了解过原理的,应该知道事务消息还有回查机制,那么这种回查机制如何去执行呢,后续分析,现在看看,当提交和回滚的时候,Broker进行了什么操作
- 成功:转换成真正的消息,然后提交到真正的主题下面,删除半消息
- 回滚:通过消息偏移量获取到消息,然后删除半消息
- 未知:没有处理
scss//K1 提交的操作 if (MessageSysFlag.TRANSACTION_COMMIT_TYPE == requestHeader.getCommitOrRollback()) { result = this.brokerController.getTransactionalMessageService().commitMessage(requestHeader); if (result.getResponseCode() == ResponseCode.SUCCESS) { RemotingCommand res = checkPrepareMessage(result.getPrepareMessage(), requestHeader); if (res.getCode() == ResponseCode.SUCCESS) { //K2 获取真正的消息 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); //K2 执行落盘操作 RemotingCommand sendResult = sendFinalMessage(msgInner); //K2 执行成功了删除办消息 if (sendResult.getCode() == ResponseCode.SUCCESS) { this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); } return sendResult; } return res; } //K1 回滚的操作 } 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); //K2 删除半消息 if (res.getCode() == ResponseCode.SUCCESS) { this.brokerController.getTransactionalMessageService().deletePrepareMessage(result.getPrepareMessage()); } return res; } } response.setCode(result.getResponseCode()); response.setRemark(result.getResponseRemark()); return response;
回查机制
- 因为commit 和 rollback 的消息是 oneway,存在丢失风险,同时,在处理提交请求的时候,并没有对 UNKWON 进行处理,那么肯定需要一个机制,确保可以准确的获取到 Producer 中本地事务的执行情况
Broker端
在 Broker 端,启动了一个事务消息的检测服务 TransactionalMessageCheckService,该服务在 Broker 启动的时候启动,默认是 1 分钟,执行一次
TransactionalMessageCheckService
- 此类也是一个 ServiceThread,是一个线程对象
csharp
@Override
public void run() {
log.info("Start transaction check service thread!");
//IMP 检测间隔 1 分钟
long checkInterval = brokerController.getBrokerConfig().getTransactionCheckInterval();
while (!this.isStopped()) {
this.waitForRunning(checkInterval);
}
log.info("End transaction check service thread!");
}
onWaitEnd
-
两个重要的参数
- TransactionTimeOut:事务的过期时间,一个消息的存储时间 + 该值 > 系统当前时间,表示事务消息还在有效期内,可以对该消息执行事务状态会查。
- TransactionCheckMax:事务的最大检测次数,如果超过检测次数,消息会默认为丢弃,即rollback消息。
arduino
@Override
protected void onWaitEnd() {
//K1 事务消息的有效时间
long timeout = brokerController.getBrokerConfig().getTransactionTimeOut();
//K1 事务的最大检测次数
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);
}
TransactionalMessageServiceImpl
check
- 此方法是回查方法,由于代码太长,分开来考虑
-
根据半消息主题,查询消息队列,由于事务消息的主题是特定的,此处也可以看出来,不可以随意改动
iniString topic = TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC; //K1 根据主题名称,获取该主题下所有的消息队列 Set<MessageQueue> msgQueues = transactionalMessageBridge.fetchMessageQueues(topic); if (msgQueues == null || msgQueues.size() == 0) { log.warn("The queue of topic is empty :" + topic); return; } log.debug("Check topic={}, queues={}", topic, msgQueues);
-
循环遍历消息队列,从单个消息消费队列去获取消息。
- 此处的当前时间,非常关键,可是后面用于终止循环的逻辑
scss//K1 循环遍历消息队列,从单个消息消费队列去获取消息 for (MessageQueue messageQueue : msgQueues) { long startTime = System.currentTimeMillis(); //..... 下面将针对消费队列的逻辑 }
-
获取两个特定主题的消费进度偏移量,用于嘛,肯定是用于获取消息用的,但是这两个主题干嘛用的呢,此处特别重要。既然是回查,那么肯定需要知道哪些消息需要去回查,还有就是回查的时候,如果避免重复回查呢,就是幂等操作呢,所以此处的两个主题为
- RMQ_SYS_TRANS_OP_HALF_TOPIC:此主题主要是为了在 commit 或者 rollback 的时候,会把消息加入到此主题中,可想而知,这个主题中可以获取到,哪些消息执行完成了(包括成功和回滚)
- RMQ_SYS_TRANS_HALF_TOPIC:既然要回查,肯定需要半消息主题了,确定一下哪些消息需要回查
如果消费进度不合理,直接下一个消息队列进行消费即可
iniMessageQueue opQueue = getOpQueue(messageQueue); long halfOffset = transactionalMessageBridge.fetchConsumeOffset(messageQueue); 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; }
-
拉取操作主题【RMQ_SYS_TRANS_OP_HALF_TOPIC】中的 32 条消息,干嘛用的呢,想想上面说的,这个里面存储的执行过的消息,那么肯定是用于避免重复回查用的,不过这个地方,用两个结构记录了 已经执行完成过的事务消息,以当前的拉取进度为分界线。这两个接口可都是用于记录执行完成的事务消息
-
doneOpOffset:用于记录在拉取半消息消费进度之前已经执行完成的消息
-
removeMap:用于记录在拉取进度之后执行完成的消息。
后续肯定会发现 随着消费进度的增加,donOpOffset 越来越多, 而 removeMap 越来越少
ini//IMP 从字面上看,是保存已经回查过的偏移量,为了避免重复调用回查接口 // -------doneOpOffset-------|halfOffset|--------removeMap------- // doneOpOffset 表示,当前进度之前,已经处理过的消息进度 // removeMap 表示,对于当前进度而言,是之后的偏移量,但是已经处理过的消息偏移量 List<Long> doneOpOffset = new ArrayList<>(); //半消息偏移量 --》 OP 偏移量 HashMap<Long, Long> removeMap = new HashMap<>(); PullResult pullResult = fillOpRemoveMap(removeMap, opQueue, opOffset, halfOffset, doneOpOffset); if (null == pullResult) { log.error("The queue={} check msgOffset={} with opOffset={} failed, pullResult is null", messageQueue, halfOffset, opOffset); continue; }
- 调用【fillOpRemoveMap】主题填充removeMap、doneOpOffset数据结构,这里主要的目的是避免重复调用事务回查接口,具体的分割线,可以看下面源码
kotlinprivate PullResult fillOpRemoveMap(HashMap<Long, Long> removeMap, MessageQueue opQueue, long pullOffsetOfOp, long miniOffset, List<Long> doneOpOffset) { //K1 拉取 OP 主题的 32 条消息,从拉取的偏移量之后的 32 条 OP消息 PullResult pullResult = pullOpMsg(opQueue, pullOffsetOfOp, 32); if (null == pullResult) { return null; } 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; } //OP 中的消息,表示已经 提交火滚的消息,就是已经处理过了的消息,无需再次操作 for (MessageExt opMessageExt : opMsg) { Long queueOffset = getLong(new String(opMessageExt.getBody(), TransactionalMessageUtil.charset)); log.debug("Topic: {} tags: {}, OpOffset: {}, HalfOffset: {}", opMessageExt.getTopic(), opMessageExt.getTags(), opMessageExt.getQueueOffset(), queueOffset); //k2 commit 成功或者 rollback 成功的消息 if (TransactionalMessageUtil.REMOVETAG.equals(opMessageExt.getTags())) { if (queueOffset < miniOffset) { //IMP 过去式中已经处理过的消息 doneOpOffset.add(opMessageExt.getQueueOffset()); } else { //IMP 已经处理过了,但是消息队列认为没有处理的消息 removeMap.put(queueOffset, opMessageExt.getQueueOffset()); } } else { log.error("Found a illegal tag in opMessageExt= {} ", opMessageExt); } } log.debug("Remove map: {}", removeMap); log.debug("Done op list: {}", doneOpOffset); return pullResult; }
-
-
定义几个私有变量,用于后面进行偏移量的更新操作,这样不会改变原始的变量的值,可以进行比较等操作
ini//IMP 获取空消息的次数 int getMessageNullCount = 1; //IMP 当前处理 事务半消息主题 【RMQ_SYS_TRANS_HALF_TOPIC#queueId】 的最新进度 long newOffset = halfOffset; //IMP 当前处理消息的队列偏移量,其主题依然为RMQ_SYS_TRANS_HALF_TOPIC long i = halfOffset;
-
针对消息队列进行,自旋操作,还记得之前的那个开始时间吗,所以,在自旋的过程中,如果超时了,则放弃当前消息队列的执行,那么很显然,每隔消息队列的自旋时间有上限,很好的避免了不断的死循环逻辑
cwhile (true) { //K2 做事务状态回查,一次最多不超过60S,目前该值不可配置 if (System.currentTimeMillis() - startTime > MAX_PROCESS_TIME_LIMIT) { log.info("Queue={} process time reach max={}", messageQueue, MAX_PROCESS_TIME_LIMIT); break; } //.... }
-
如果当前的半小时偏移量,存在于 removeMap 中,也就是已经执行成功了,那么只需要更新拉取偏移量就好了,然后自旋执行下一次拉取的逻辑。如果不在呢,可能有很多中可能
- 消息真的没有执行
- 消息执行了,只是你在获取 【RMQ_SYS_TRANS_OP_HALF_TOPIC】 判断是否执行的时候,消息还没有加入到此主题中,那么就是 else 的逻辑,也是此处,最复杂的逻辑,累了可以休息一波,确实好多内容
csharp//K2 如果已经处理过了,就直接跳过,执行下一条消息如果removeMap中包含当前处理的消息,则继续下一条 if (removeMap.containsKey(i)) { log.debug("Half offset {} has been committed/rolled back", i); Long removedOpOffset = removeMap.remove(i); doneOpOffset.add(removedOpOffset); }else{ //.... }
累了休息会儿,此处打个分割符,方便后面回来继续查看
回查负责逻辑
-
先从半消息主题队列中获取消息
ini//K2 根据消息队列偏移量i从消费队列中获取消息 GetResult getResult = getHalfMsg(messageQueue, i); MessageExt msgExt = getResult.getMsg();
-
下面情况则执行下一个半消息队列的回查操作
- 没有数据,而且 获取空消息的次数操作最大次数,此处为 1
- 如果拉取到的消息状态,给定是没有新的消息,那么也执行下一个队列的操作
- 其他原因,则将偏移量i设置为: getResult.getPullResult().getNextBeginOffset(),重新拉取
arduino//K2 如果消息为空,则根据允许重复次数进行操作,默认重试一次 if (msgExt == null) { //MAX_RETRY_COUNT_WHEN_HALF_NULL = 1 //k2 如果超过重试次数,直接跳出,结束该消息队列的事务状态回查。 if (getMessageNullCount++ > MAX_RETRY_COUNT_WHEN_HALF_NULL) { //IMP 跳出当前队列的处理,继续下一个消息队列 break; } //K2 如果是由于没有新的消息而返回为空(拉取状态为:PullStatus.NO_NEW_MSG),则结束该消息队列的事务状态回查 if (getResult.getPullResult().getPullStatus() == PullStatus.NO_NEW_MSG) { log.debug("No new msg, the miss offset={} in={}, continue check={}, pull result={}", i, messageQueue, getMessageNullCount, getResult.getPullResult()); //IMP 跳出当前队列的处理,继续下一个消息队列 break; } else { //k2 其他原因,则将偏移量i设置为: getResult.getPullResult().getNextBeginOffset(),重新拉取 log.info("Illegal offset, the miss offset={} in={}, continue check={}, pull result={}", i, messageQueue, getMessageNullCount, getResult.getPullResult()); i = getResult.getPullResult().getNextBeginOffset(); newOffset = i; continue; } }
-
如果拉取到消息呢,肯定需要判断消息是否需要回查,或者是否需要跳过,此处会涉及到两个方法 【needDiscard】和 【needSkip】,此处注意一个参数,transactionCheckMax,是设置的最多的事务回查次数,如果太多次没有结果,我就放弃了,确实如此哈
- needDiscard:如果该消息回查的次数超过允许的最大回查次数,则该消息将被丢弃,即事务消息提交失败,不能被消费者消费,其做法,主要是每回查一次,在消息属性TRANSACTION_CHECK_TIMES中增1,默认最大回查次数为5次
- needSkip依据:如果事务消息超过文件的过期时间,默认72小时(具体请查看RocketMQ过期文件相关内容),则跳过该消息
less//K2 判断该消息是否需要discard(吞没,丢弃,不处理)、或skip(跳过) // transactionCheckMax 最大的拉取次数 if (needDiscard(msgExt, transactionCheckMax) || needSkip(msgExt)) { listener.resolveDiscardMsg(msgExt); newOffset = i + 1; i++; continue; } if (msgExt.getStoreTimestamp() >= startTime) { log.debug("Fresh stored. the miss offset={}, check it later, store={}", i, new Date(msgExt.getStoreTimestamp())); break; }
接下来是这里面最不好理解的地方,如果不清醒了,休息会再看
-
先了解几个变量
-
valueOfCurrentMinusBorn:当前时间 - 消息产生的时间,那么就是半消息存活的时间了,为啥要有这个东西呢,考虑一个点,如果半消息刚落到半消息主题上,你就立刻去回查,大概率是没有结果的,所以此值, 用于避免无意义的回查操作
-
checkImmunityTime:既然为了避免无意义的回查,那么多久之后认为是有意义的呢,好吧,这个值就是那个意义值。如果小于这个值,则认为太快查询了,停止查询,等待下次查询吧
- checkImmunityTimeStr: 这个值呢,就是用户设置的上面的太长时间的数值表示,表示超了这个时间呢,就立刻回查,此值是配置信息,如果设置了,就更新 checkImmunityTime 的值
-
transactionTimeout:事务消息的超时时间,这个时间是从OP拉取的消息的最后一条消息的存储时间与check方法开始的时间,如果时间差超过了transactionTimeout,就算时间小于checkImmunityTime时间,也发送事务回查指令
ini//IMP 该消息已存储的时间,等于系统当前时间减去消息存储的时间戳 long valueOfCurrentMinusBorn = System.currentTimeMillis() - msgExt.getBornTimestamp(); //IMP 立即检测事务消息的时间,延迟时间,如果时间太短,还没有来得及提交或者回滚,就去查询了 // ,其设计的意义是,应用程序在发送事务消息后,事务不会马上提交,该时间就是假设事务消息发送成功后,应用程序事务提交的时间 // ,在这段时间内,RocketMQ任务事务未提交,故不应该在这个时间段向应用程序发送回查请求。 long checkImmunityTime = transactionTimeout; //事务消息过期时间属性(PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS) String checkImmunityTimeStr = msgExt.getUserProperty(MessageConst.PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS);
-
-
如果当前时间还未过(应用程序事务结束时间),则跳出本次回查处理的,等下一次再试
- valueOfCurrentMinusBorn < checkImmunityTime,土话说,你太急了,再等等,对方可能在回复中
scssif (null != checkImmunityTimeStr) { checkImmunityTime = getImmunityTime(checkImmunityTimeStr, transactionTimeout); if (valueOfCurrentMinusBorn < checkImmunityTime) { if (checkPrepareQueueOffset(removeMap, doneOpOffset, msgExt)) { newOffset = i + 1; i++; continue; } } } else { //IMP 小于规定的最小检测时间,说明时间太短,此次不用回查,每隔事务都有一个执行周期, // 周期内,说明还没有执行完,无需进行回查,避免了无效的回查操作 if ((0 <= valueOfCurrentMinusBorn) && (valueOfCurrentMinusBorn < checkImmunityTime)) { log.debug("New arrived, the miss offset={}, check it later checkImmunity={}, born={}", i, checkImmunityTime, new Date(msgExt.getBornTimestamp())); break; } }
-
走到这里呢,一定是 valueOfCurrentMinusBorn > checkImmunityTime 肯定是当前消息的存活时间已经超了 checkImmunityTime 的时间点,但是 MQ 系统仍然有个时间点 transactionTimeout, 一种情况,如果设置的太小,岂不是还是不断的去回查消息,所以可以理解为另一种保障。下面就是判断是否需要回查的逻辑 ,直接上代码
scss// 说明有消息已经 commit 或者 rollback 了 List<MessageExt> opMsg = pullResult.getMsgFoundList(); boolean isNeedCheck = //IMP 情况一:很久没有消息反馈的,必须回查 //如果从操作队列(RMQ_SYS_TRANS_OP_HALF_TOPIC)中没有已处理消息 // 并且消息已经执行很久了(应用程序事务结束时间),参数transactionTimeOut值。 // 那么次消息肯定要回查 (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime) //如果操作队列不为空,并且最后一条消息的存储时间已经超过transactionTimeOut值。 //IMP 情况二:已经有处理完的消息,而且当前消息不在处理消息的范围内,如果最后一条消息已经超时了,那么当前消息肯定也超时了,必须回查 || (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout)) || (valueOfCurrentMinusBorn <= -1);
-
拉取的 32 条已经执行完成的事务消息
-
下面两种情况需要回查
- 操作队列为空,而且很久没有写入消息了,那么必须回查
- 操作队列不为空,而且最后一条消息也已经超了 回查的超时时间了,那么没有当前消息的我,是不是应该回查一下我这条消息的消费情况 了
-
-
如果不满足回查条件,怎么办呢,有可能是我拉取的时候没有,可能现在就有了,那么我继续下一个偏移量,跟新我的已经处理好的事务消息的结果,然后继续去判断上面的逻辑,这也就是为什么要加个时间限制,否则一直一个队列不断判断回查了,不就是死循环了吗
ini//K1 如果无法判断是否发送回查消息,则加载更多的已处理消息进行刷选,如果执行时间太长,就等待再次拉取之后再来判断 // 否则就会出现重复拉取的情况了 pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset); log.debug("The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}", i, messageQueue, pullResult); continue;
-
如果满足回查条件呢,执行 l 下面的代码,有个疑问你发就发吧,还重新 put 了一遍是啥意思
kotlin //IMP 如果需要发送事务状态回查消息,则先将消息再次发送到RMQ_SYS_TRANS_HALF_TOPIC主题中,发送成功则返回true,否则返回false if (!putBackHalfMsgQueue(msgExt, i)) { continue; } //K1 异步线程发送回查消息 listener.resolveHalfMsg(msgExt);
putBackHalfMsgQueue
lessprivate boolean putBackHalfMsgQueue(MessageExt msgExt, long offset) { //K1 重新将当前消息加入到半消息中 PutMessageResult putMessageResult = putBackToHalfQueueReturnResult(msgExt); //K1 如果发送成功,会将该消息的queueOffset、commitLogOffset设置为重新存入的偏移量 if (putMessageResult != null && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) { //K2 发送成功,会将该消息的queueOffset、commitLogOffset设置为重新存入的偏移量 msgExt.setQueueOffset( putMessageResult.getAppendMessageResult().getLogicsOffset()); msgExt.setCommitLogOffset( putMessageResult.getAppendMessageResult().getWroteOffset()); msgExt.setMsgId(putMessageResult.getAppendMessageResult().getMsgId()); log.debug( "Send check message, the offset={} restored in queueOffset={} " + "commitLogOffset={} " + "newMsgId={} realMsgId={} topic={}", offset, msgExt.getQueueOffset(), msgExt.getCommitLogOffset(), msgExt.getMsgId(), msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX), msgExt.getTopic()); return true; } else { log.error( "PutBackToHalfQueueReturnResult write failed, topic: {}, queueId: {}, " + "msgId: {}", msgExt.getTopic(), msgExt.getQueueId(), msgExt.getMsgId()); return false; } }
-
通过上面代码,为啥要有这个逻辑呀,想一个点,既然回查肯定不能阻塞回查的执行,
-
那么必然有个情况,如果消息回查异常了怎么办,如果来重复回查呢,所以,此处将消息重新加入到了 半消息队列中。有没有一个可能,我回查成功了,但是呢现在又加入了一遍,会不会重复回查呢,当然不会了,忘了我们 removeMap 了吗,就是为了避免重复回查的。
-
但是现在又有一个问题,removeMap 可是我拉取 32 条数据增加的消息过滤 map,怎么能保障一定能拉取到处理记录呢,也就是存在于 OP主题中呢???
-
是否记得上面的一个逻辑【(opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout)】,如果发现回查的速度太快,拉取的时候,只是拉取了部分的消息处理数据,那么需要继续去拉取,只有等超了 事务回查超时时间,才认为是确实需要回查,所以此处消息回查
- 如果确实超了时间,那么肯定去回查
- 如果没有超了时间,就去拉取递增的拉取处理结果,知道拉取到所有超时之前的所有处理结果
-
-
resolveHalfMsg
最后向 Producer 发送了回查的 request ,requestCode 为【CHECK_TRANSACTION_STATE】
javapublic void resolveHalfMsg(final MessageExt msgExt) { executorService.execute(new Runnable() { @Override public void run() { try { sendCheckMessage(msgExt); } catch (Exception e) { LOGGER.error("Send check message error!", e); } } }); } public void sendCheckMessage(MessageExt msgExt) throws Exception { //K1 首先构建回查事务状态请求消息,请求核心参数包括:消息offsetId、消息ID(索引)、消息事务ID、事务消息队列中的偏移量(RMQ_SYS_TRANS_HALF_TOPIC) CheckTransactionStateRequestHeader checkTransactionStateRequestHeader = new CheckTransactionStateRequestHeader(); 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))); //K1 恢复原消息的主题、队列,并设置storeSize为0 msgExt.setStoreSize(0); //K1 获取生产者组名称 String groupId = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); //K1 根据生产者组获取任意一个生产者,通过与其连接发送事务回查消息,回查消息的请求者为【Broker服务器】,接收者为(client,具体为消息生产者) Channel channel = brokerController.getProducerManager().getAvailableChannel(groupId); if (channel != null) { brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt); } else { LOGGER.warn("Check transaction failed, channel is null. groupId={}", groupId); } } public void checkProducerTransactionState( final String group, final Channel channel, final CheckTransactionStateRequestHeader requestHeader, final MessageExt messageExt) throws Exception { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CHECK_TRANSACTION_STATE, requestHeader); request.setBody(MessageDecoder.encode(messageExt, false)); try { this.brokerController.getRemotingServer().invokeOneway(channel, request, 10); } catch (Exception e) { log.error("Check transaction failed because invoke producer exception. group={}, msgId={}, error={}", group, messageExt.getMsgId(), e.toString()); } }
-
-
跟新进度信息,半消息进度,和处理结果队列消费进度,这样就可以一直执行下去
iniif (newOffset != halfOffset) { //K1 保存(Prepare)消息队列的回查进度 transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset); } long newOpOffset = calculateOpOffset(doneOpOffset, opOffset); if (newOpOffset != opOffset) { //K1 保存处理队列(op)的进度 transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset); }
引用借鉴学习:blog.csdn.net/prestigedin...,blog.csdn.net/prestigedin...