boolean isNeedCheck = (opMsg == null && valueOfCurrentMinusBorn > checkImmunityTime)
|| (opMsg != null && (opMsg.get(opMsg.size() - 1).getBornTimestamp() - startTime > transactionTimeout))
|| (valueOfCurrentMinusBorn <= -1); // @10
if (isNeedCheck) {
if (!putBackHalfMsgQueue(msgExt, i)) { // @11
continue;
}
listener.resolveHalfMsg(msgExt);
} else {
pullResult = fillOpRemoveMap(removeMap, opQueue, pullResult.getNextBeginOffset(), halfOffset, doneOpOffset); // @12
log.info(“The miss offset:{} in messageQueue:{} need to get more opMsg, result is:{}”, i,
messageQueue, pullResult);
continue;
}
}
newOffset = i + 1;
i++;
}
if (newOffset != halfOffset) { // @13
transactionalMessageBridge.updateConsumeOffset(messageQueue, newOffset);
}
long newOpOffset = calculateOpOffset(doneOpOffset, opOffset);
if (newOpOffset != opOffset) { // @14
transactionalMessageBridge.updateConsumeOffset(opQueue, newOpOffset);
}
本段代码比较长,却是事务状态回查的重点实现。
代码@1:先解释几个局部变量的含义。
getMessageNullCount :获取空消息的次数。
newOffset :当前处理RMQ_SYS_TRANS_HALF_TOPIC#queueId的最新进度。
i:当前处理消息的队列偏移量,其主题依然为RMQ_SYS_TRANS_HALF_TOPIC。
代码@2:这段代码应该不陌生,这是RocketMQ处理任务的一个通用处理逻辑,就是一个任务处理,可以限制每次最多处理的时间,RocketMQ为待检测主题RMQ_SYS_TRANS_HALF_TOPIC的每个队列,做事务状态回查,一次最多不超过60S,目前该值不可配置。
代码@3:如果removeMap中包含当前处理的消息,则继续下一条,removeMap中 的值是通过Step3中,填充的,具体实现逻辑是从RMQ_SYS_TRANS_OP_HALF_TOPIC主题中拉取32条,如果拉取的消息队列偏移量大于等于RMQ_SYS_TRANS_HALF_TOPIC#queueId当前的处理进度时,会添加到removeMap中,表示已处理过。
代码@4:根据消息队列偏移量i从消费队列中获取消息。
代码@5:如果消息为空,则根据允许重复次数进行操作,默认重试一次,目前不可配置。其具体实现为:
1、如果超过重试次数,直接跳出,结束该消息队列的事务状态回查。
2、如果是由于没有新的消息而返回为空(拉取状态为:PullStatus.NO_NEW_MSG),则结束该消息队列的事务状态回查。
3、其他原因,则将偏移量i设置为: getResult.getPullResult().getNextBeginOffset(),重新拉取。
代码@6:判断该消息是否需要discard(吞没,丢弃,不处理)、或skip(跳过),其依据如下:
1、needDiscard 依据:如果该消息回查的次数超过允许的最大回查次数,则该消息将被丢弃,即事务消息提交失败,不能被消费者消费,其做法,主要是每回查一次,在消息属性TRANSACTION_CHECK_TIMES中增1,默认最大回查次数为5次。
2、needSkip依据:如果事务消息超过文件的过期时间,默认72小时(具体请查看RocketMQ过期文件相关内容),则跳过该消息。
代码@7:处理事务超时相关概念,先解释几个局部变量:、
valueOfCurrentMinusBorn :该消息已存储的时间,等于系统当前时间减去消息存储的时间戳。
checkImmunityTime :立即检测事务消息的时间,其设计的意义是,应用程序在发送事务消息后,事务不会马上提交,该时间就是假设事务消息发送成功后,应用程序事务提交的时间,在这段时间内,RocketMQ任务事务未提交,故不应该在这个时间段向应用程序发送回查请求。
transactionTimeout:事务消息的超时时间,这个时间是从OP拉取的消息的最后一条消息的存储时间与check方法开始的时间,如果时间差超过了transactionTimeout,就算时间小于checkImmunityTime时间,也发送事务回查指令。
代码@8:如果消息指定了事务消息过期时间属性(PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS),如果当前时间已超过该值。
代码@9:如果当前时间还未过(应用程序事务结束时间),则跳出本次回查处理的,等下一次再试。
代码@10:判断是否需要发送事务回查消息,具体逻辑:
1、如果从操作队列(RMQ_SYS_TRANS_OP_HALF_TOPIC)中没有已处理消息并且已经超过(应用程序事务结束时间),参数transactionTimeOut值。
2、如果操作队列不为空,并且最后一天条消息的存储时间已经超过transactionTimeOut值。
代码@11:如果需要发送事务状态回查消息,则先将消息再次发送到RMQ_SYS_TRANS_HALF_TOPIC主题中,发送成功则返回true,否则返回false,这里还有一个实现关键点:
if (putMessageResult != null
&& putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
msgExt.setQueueOffset(
putMessageResult.getAppendMessageResult().getLogicsOffset());
msgExt.setCommitLogOffset(
putMessageResult.getAppendMessageResult().getWroteOffset());
msgExt.setMsgId(putMessageResult.getAppendMessageResult().getMsgId());
}
如果发送成功,会将该消息的queueOffset、commitLogOffset设置为重新存入的偏移量,为什么需要这样呢,答案在listener.resolveHalfMsg(msgExt)中。
AbstractTransactionalMessageCheckListener#resolveHalfMsg
public 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);
}
}
});
}
发送具体的事务回查机制,这里用一个线程池来异步发送回查消息,为了回查进度保存的简化,这里只要发送了回查消息,当前回查进度会向前推动,如果回查失败,上一步骤新增的消息将可以再次发送回查消息,那如果回查消息发送成功,那会不会下一次又重复发送回查消息呢?这个可以根据OP队列中的消息来判断是否重复,如果回查消息发送成功并且消息服务器完成提交或回滚操作,这条消息会发送到OP队列中,然后首先会通过fillOpRemoveMap根据处理进度获取一批已处理的消息,来与消息判断是否重复,由于fillopRemoveMap一次只拉32条消息,那又如何保证一定能拉取到与当前消息的处理记录呢?其实就是通过代码@10,如果此批消息最后一条未超过事务延迟消息,则继续拉取更多消息进行判断(@12)和(@14),op队列也会随着回查进度的推进而推进。
代码@12:如果无法判断是否发送回查消息,则加载更多的已处理消息进行刷选。
代码@13:保存(Prepare)消息队列的回查进度。
代码@14:保存处理队列(op)的进度。
上述讲解了TransactionalMessageCheckService回查定时线程的发送回查消息的整体流程与实现细节,接下来重点分析一下上述步骤@11,通过异步方式发送消息回查的实现过程。
AbstractTransactionalMessageCheckListener#sendCheckMessage
public void sendCheckMessage(MessageExt msgExt) throws Exception {
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()); // @1
msgExt.setTopic(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_TOPIC));
msgExt.setQueueId(Integer.parseInt(msgExt.getUserProperty(MessageConst.PROPERTY_REAL_QUEUE_ID)));
msgExt.setStoreSize(0); // @2
String groupId = msgExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP); // @3
Channel channel = brokerController.getProducerManager().getAvaliableChannel(groupId);
if (channel != null) {
brokerController.getBroker2Client().checkProducerTransactionState(groupId, channel, checkTransactionStateRequestHeader, msgExt); // @4
} else {
LOGGER.warn(“Check transaction failed, channel is null. groupId={}”, groupId);
}
}
代码@1:首先构建回查事务状态请求消息,请求核心参数包括:消息offsetId、消息ID(索引)、消息事务ID、事务消息队列中的偏移量(RMQ_SYS_TRANS_HALF_TOPIC)。
代码@2:恢复原消息的主题、队列,并设置storeSize为0。
代码@3:获取生产者组名称。
代码@4:根据生产者组获取任意一个生产者,通过与其连接发送事务回查消息,回查消息的请求者为【Broker服务器】,接收者为(client,具体为消息生产者)。
其处理类为:org.apache.rocketmq.client.impl.ClientRemotingProcessor#processRequest,其详细逻辑实现方法为:
ClientRemotingProcessor#checkTransactionState
public RemotingCommand checkTransactionState(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
final CheckTransactionStateRequestHeader requestHeader =
(CheckTransactionStateRequestHeader) request.decodeCommandCustomHeader(CheckTransactionStateRequestHeader.class);
final ByteBuffer byteBuffer = ByteBuffer.wrap(request.getBody());
final MessageExt messageExt = MessageDecoder.decode(byteBuffer);
if (messageExt != null) {
String transactionId = messageExt.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
if (null != transactionId && !“”.equals(transactionId)) {
messageExt.setTransactionId(transactionId);
}
final String group = messageExt.getProperty(MessageConst.PROPERTY_PRODUCER_GROUP);
if (group != null) {
MQProducerInner producer = this.mqClientFactory.selectProducer(group);
if (producer != null) {
final String addr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
producer.checkTransactionState(addr, messageExt, requestHeader); // @1
} else {
log.debug(“checkTransactionState, pick producer by group[{}] failed”, group);
}
} else {
log.warn(“checkTransactionState, pick producer group failed”);
}
} else {
log.warn(“checkTransactionState, decode message failed”);
}
return null;
}
代码@1:最终调用生产者的checkTransactionState方法。
DefaultMQProducerImpl#checkTransactionState
public void checkTransactionState(final String addr, final MessageExt msg,
final CheckTransactionStateRequestHeader header) {
Runnable request = new Runnable() { // @1
private final String brokerAddr = addr;
private final MessageExt message = msg;
private final CheckTransactionStateRequestHeader checkRequestHeader = header;
private final String group = DefaultMQProducerImpl.this.defaultMQProducer.getProducerGroup();
@Override
public void run() {
TransactionListener transactionCheckListener = DefaultMQProducerImpl.this.checkListener(); // @1
if (transactionCheckListener != null) {
LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
Throwable exception = null;
try {
localTransactionState = transactionCheckListener.checkLocalTransaction(message); // @2
} catch (Throwable e) {
log.error(“Broker call checkTransactionState, but checkLocalTransactionState exception”, e);
exception = e;
}
this.processTransactionState( // @3
localTransactionState,
group,
exception);
} else {
log.warn(“checkTransactionState, pick transactionCheckListener by group[{}] failed”, group);
}
}
private void processTransactionState(
final LocalTransactionState localTransactionState,
final String producerGroup,
final Throwable exception) {
final EndTransactionRequestHeader thisHeader = new EndTransactionRequestHeader();
thisHeader.setCommitLogOffset(checkRequestHeader.getCommitLogOffset());
thisHeader.setProducerGroup(producerGroup);
thisHeader.setTranStateTableOffset(checkRequestHeader.getTranStateTableOffset());
thisHeader.setFromTransactionCheck(true);
String uniqueKey = message.getProperties().get(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
if (uniqueKey == null) {
uniqueKey = message.getMsgId();
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
最后
这份《“java高分面试指南”-25分类227页1000+题50w+字解析》同样可分享给有需要的朋友,感兴趣的伙伴们可挑战一下自我,在不看答案解析的情况,测试测试自己的解题水平,这样也能达到事半功倍的效果!(好东西要大家一起看才香)
的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!**
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-HmWvHbWG-1711718644228)]
最后
这份《“java高分面试指南”-25分类227页1000+题50w+字解析》同样可分享给有需要的朋友,感兴趣的伙伴们可挑战一下自我,在不看答案解析的情况,测试测试自己的解题水平,这样也能达到事半功倍的效果!(好东西要大家一起看才香)
[外链图片转存中…(img-HQYgaEfc-1711718644229)]
[外链图片转存中…(img-BMFbwTbB-1711718644229)]