本文图片和部分总结来自于参考资料,半原创,侵删

问题

  • Rocketmq 重试是否有超时问题,假如超时了如何解决,是重新发送消息呢?还是一直等待
  • 假如某个 msg 进入了重试队列(%RETRY_XXX%),然后成功消费了

概述

    文章介绍了RocketMQ 的重试机制和消息重试的机制。

定时任务

定时任务概述

    rocketmq为定时任务创建一个单独的 topic ,而 rocketmq的定时任务是定的时间是分等级的,而不同等级对应topic内不同的队列,然后通过一个“执行定时任务的服务”定时执行多个队列内的任务,执行时需要更改该定时任务实际要发送的 topic 和 tag 。

发送例子

发送例子

Message msg =
new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */);
msg.setDelayTimeLevel(i + 1);

    时间等级

public class MessageStoreConfig {

    private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

}

写入定时任务

    写入的时候是在写入commitLog 的时候写入的,这一点很重要,因为这也是实现消费失败重试的基础。 CommitLog 会将这条消息的话题和队列 ID 替换成专门用于定时的话题和相应的级别对应的队列 ID。真实的话题和队列 ID 会作为属性放置到这条消息中,后面处理的时候会自己从这个队列id 进行发送消息。

    public class CommitLog {

        public PutMessageResult putMessage(final MessageExtBrokerInner msg) {

            // Delay Delivery
if (msg.getDelayTimeLevel() > 0) { topic = ScheduleMessageService.SCHEDULE_TOPIC;
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel()); // Backup real topic, queueId
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())); // 替换 Topic 和 QueueID
msg.setTopic(topic);
msg.setQueueId(queueId);
} } }

处理定时任务

    执行定时任务的服务,ScheduleMessageService 的 start 方法

    public void start() {

        for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
Integer level = entry.getKey();
Long timeDelay = entry.getValue();
Long offset = this.offsetTable.get(level);
if (null == offset) {
offset = 0L;
} if (timeDelay != null) {
//Timer 持有多个定时任务,然后时间到了就执行该任务,
// 但是 Timer 内部只有一个线程在执行任务,也就不能保证时间的正确性(因为当一个线程在执行的时候,某个任务的时间已经到了)
// 注意,是为每个延时时间等级建一个任务Task
this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
}
} this.timer.scheduleAtFixedRate(new TimerTask() { @Override
public void run() {
try {
ScheduleMessageService.this.persist();
} catch (Throwable e) {
log.error("scheduleAtFixedRate flush exception", e);
}
}
}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
}
class DeliverDelayedMessageTimerTask extends TimerTask {

    public void executeOnTimeup() {
// ...
for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
// 是否到时间
long countdown = deliverTimestamp - now; if (countdown <= 0) {
// 取出消息
MessageExt msgExt =
ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(offsetPy, sizePy);
// 修正消息,设置上正确的话题和队列 ID
MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
// 重新存储消息
PutMessageResult putMessageResult =
ScheduleMessageService.this.defaultMessageStore
.putMessage(msgInner);
} else {
// countdown 后投递此消息
ScheduleMessageService.this
.timer
.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset), countdown);
// 更新偏移量
}
} // end of for // 更新偏移量
} }

    同时该定时任务也进行持久化,一个是消费进度,一个消息对应的位移量

消息消费重试

    RocketMQ中遇到以下情况就会进行消息重试 :

  • 抛出异常
  • 返回 NULL 状态
  • 返回 RECONSUME_LATER 状态
  • 超时 15 分钟没有响应

consumer 注册订阅重试队列

    consumer 在启动的时候就会订阅“%RETRY_XXX%”的topic,为的就是当某个topic消费失败处理重试消息。如下图所示 :

public class DefaultMQPushConsumerImpl implements MQConsumerInner {

    public synchronized void start() throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
// ...
this.copySubscription();
// ... this.serviceState = ServiceState.RUNNING;
break;
}
} private void copySubscription() throws MQClientException {
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
break; case CLUSTERING:
// 重试话题组
final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup());
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(this.defaultMQPushConsumer.getConsumerGroup(),
retryTopic, SubscriptionData.SUB_ALL);
this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData);
break; default:
break;
}
} }

超时消费

    我们思考一个问题,假如消费者掉线了,那么消息直接发不过去了,而要是消费者的消费逻辑执行了太久的业务逻辑,那么应该有一个动作来触发 消费超时,进行重试.

ConsumeMessageConcurrentlyService 的 start 方法。

    public void start() {
this.cleanExpireMsgExecutors.scheduleAtFixedRate(new Runnable() { @Override
public void run() {
cleanExpireMsg();
} }, this.defaultMQPushConsumer.getConsumeTimeout(), this.defaultMQPushConsumer.getConsumeTimeout(), TimeUnit.MINUTES);
}

这个定时周期任务每过 getConsumeTimeout 时间就会扫描消费超时的任务,调用 sendMessageBack 方法,该方法会调用 RPC发送消息给 broker ,消费失败进行重试。

    上一篇我们讲到消息消费的过程,当集群模式下,消息消费成功会本地的消息消费进度,而失败了会调用RPC 发送消息给broker ,而broker 处理的逻辑在 SendMessageProcessor

    @Override
public RemotingCommand processRequest(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
SendMessageContext mqtraceContext;
switch (request.getCode()) { //消费者消费失败的情况
case RequestCode.CONSUMER_SEND_MSG_BACK:
return this.consumerSendMsgBack(ctx, request);
default: SendMessageRequestHeader requestHeader = parseRequestHeader(request);
if (requestHeader == null) {
return null;
} mqtraceContext = buildMsgContext(ctx, requestHeader);
this.executeSendMessageHookBefore(ctx, request, mqtraceContext); RemotingCommand response;
if (requestHeader.isBatch()) {
response = this.sendBatchMessage(ctx, request, mqtraceContext, requestHeader);
} else {
response = this.sendMessage(ctx, request, mqtraceContext, requestHeader);
} this.executeSendMessageHookAfter(response, mqtraceContext);
return response;
}
}

  需要注意的是 consumerTimeOut 的时间是 15 分钟,生产的时候可以配置短点。

批量处理的问题

    批量处理一批数据要是返回 RECONSUME_LATER ,那么这批数据就会重新发给 broker ,进行消息重试,所以在业务逻辑的时候就要考虑消费者重新消费的幂等性。

    ConsumeRequest的 run 方法

        @Override
public void run() {
.... try {
ConsumeMessageConcurrentlyService.this.resetRetryTopic(msgs);
if (msgs != null && !msgs.isEmpty()) {
for (MessageExt msg : msgs) {
MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis()));
}
}
//NO.1 业务实现
status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);
} catch (Throwable e) {
log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
RemotingHelper.exceptionSimpleDesc(e),
ConsumeMessageConcurrentlyService.this.consumerGroup,
msgs,
messageQueue);
hasException = true;
} ... if (!processQueue.isDropped()) {
//NO.2 处理消息消费的结果
ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
} else {
log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs);
}
}

我们可以设置最大批量处理的数量为 1 ,那么就会针对每一条消息进行重试,但是那样的话就会性能相对于批量处理肯定差一些。

ack 机制

    public void processConsumeResult(
final ConsumeConcurrentlyStatus status,
final ConsumeConcurrentlyContext context,
final ConsumeRequest consumeRequest
) {
int ackIndex = context.getAckIndex(); if (consumeRequest.getMsgs().isEmpty())
return; switch (status) {
case CONSUME_SUCCESS:
if (ackIndex >= consumeRequest.getMsgs().size()) {
ackIndex = consumeRequest.getMsgs().size() - 1;
}
int ok = ackIndex + 1;
int failed = consumeRequest.getMsgs().size() - ok;
this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), ok);
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), failed);
break;
case RECONSUME_LATER:
ackIndex = -1;
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(),
consumeRequest.getMsgs().size());
break;
default:
break;
} switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
MessageExt msg = consumeRequest.getMsgs().get(i);
log.warn("BROADCASTING, the message consume failed, drop it, {}", msg.toString());
}
break;
case CLUSTERING:
//发送给broker , 该批数据进行消息重试
List<MessageExt> msgBackFailed = new ArrayList<MessageExt>(consumeRequest.getMsgs().size());
for (int i = ackIndex + 1; i < consumeRequest.getMsgs().size(); i++) {
MessageExt msg = consumeRequest.getMsgs().get(i);
boolean result = this.sendMessageBack(msg, context);
if (!result) {
msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
msgBackFailed.add(msg);
}
} if (!msgBackFailed.isEmpty()) {
consumeRequest.getMsgs().removeAll(msgBackFailed); this.submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue());
}
break;
default:
break;
} //删除消息树中的已消费的消息节点,并返回消息树中最小的节点,更新最小的节点为当前进度!!
long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
}
}

可以看到消息消费完成后,更新的进度都是对应的 processqueue中对应的消息树里的最小节点(即偏移量最小的节点),那么有可能存在这样的问题,下面来自 参考

这钟方式和传统的一条message单独ack的方式有本质的区别。性能上提升的同时,会带来一个潜在的重复问题——由于消费进度只是记录了一个下标,就可能出现拉取了100条消息如 2101-2200的消息,后面99条都消费结束了,只有2101消费一直没有结束的情况。

在这种情况下,RocketMQ为了保证消息肯定被消费成功,消费进度职能维持在2101,直到2101也消费结束了,本地的消费进度才能标记2200消费结束了(注:consumerOffset=2201)。

在这种设计下,就有消费大量重复的风险。如2101在还没有消费完成的时候消费实例突然退出(机器断电,或者被kill)。这条queue的消费进度还是维持在2101,当queue重新分配给新的实例的时候,新的实例从broker上拿到的消费进度还是维持在2101,这时候就会又从2101开始消费,2102-2200这批消息实际上已经被消费过还是会投递一次。

总结

    从参考资料中我学习到了自己学习与别人的差异是总结的能力,通过浓缩代码片段,总结核心的逻辑步骤,加深对逻辑的理解。

参考资料

  • https://www.jianshu.com/p/5843cdcd02aa
  • http://jaskey.github.io/blog/2017/01/25/rocketmq-consume-offset-management/

消息队列(七)--- RocketMQ延时发送和消息重试(半原创)的更多相关文章

  1. (八)RabbitMQ消息队列-通过Topic主题模式分发消息

    原文:(八)RabbitMQ消息队列-通过Topic主题模式分发消息 前两章我们讲了RabbitMQ的direct模式和fanout模式,本章介绍topic主题模式的应用.如果对direct模式下通过 ...

  2. [分布式学习]消息队列之rocketmq笔记

    文档地址 RocketMQ架构 哔哩哔哩上的视频 mq有很多,近期买了<分布式消息中间件实践>这本书,学习关于mq的相关知识.mq大致有有4个功能: 异步处理.比如业务端需要给用户发送邮件 ...

  3. 消息队列之-RocketMQ入门

    简介 RocketMQ是阿里开源的消息中间件,目前已经捐献个Apache基金会,它是由Java语言开发的,具备高吞吐量.高可用性.适合大规模分布式系统应用等特点,经历过双11的洗礼,实力不容小觑. 官 ...

  4. RocketMQ(6)---发送普通消息(三种方式)

    发送普通消息(三种方式) RocketMQ 发送普通消息有三种实现方式:可靠同步发送.可靠异步发送.单向(Oneway)发送. 注意 :顺序消息只支持可靠同步发送. GitHub地址: https:/ ...

  5. redis实现消息队列(七)

    1. 介绍 redis有一个数据类型叫list(列表),它的每个子元素都是 string 类型的双向链表.我们可以通过 push,pop 操作从链表的头部或者尾部添加删除元素.这使得 list 既可以 ...

  6. Azure Messaging-ServiceBus Messaging消息队列技术系列4-复杂对象消息是否需要支持序列化和消息持久化

    在上一篇中,我们介绍了消息的顺序收发保证: Azure Messaging-ServiceBus Messaging消息队列技术系列3-消息顺序保证 在本文中我们主要介绍下复杂对象消息是否需要支持序列 ...

  7. 【消息队列】kafka是如何保证消息不被重复消费的

    一.kafka自带的消费机制 kafka有个offset的概念,当每个消息被写进去后,都有一个offset,代表他的序号,然后consumer消费该数据之后,隔一段时间,会把自己消费过的消息的offs ...

  8. 获取和设置消息队列的属性msgctl,删除消息队列

    消息队列的属性保存在系统维护的数据结构msqid_ds中,用户可以通过函数msgctl获取或设置消息队列的属性. int msgctl(int msqid, int cmd, struct msqid ...

  9. RabbitMQ消息队列入门(一)——RabbitMQ消息队列的安装(Windows环境下)

    一.RabbitMQ介绍1.RabbitMQ简介RabbitMQ是一个消息代理:它接受和转发消息.你可以把它想象成一个邮局:当你把你想要发布的邮件放在邮箱中时,你可以确定邮差先生最终将邮件发送给你的收 ...

随机推荐

  1. 《NVM-Express-1_4-2019.06.10-Ratified》学习笔记(5.2)-- Asynchronous Event Request command

    5.2 异步事件请求命令 异步事件用于当状态.错误.健康信息这些事件发生时通知主机软件.为了使能这个controller报告的异步事件,主机软件需要提交一个或多个异步事件请求命令到controller ...

  2. Babel 7 主要改变

    1.不支持Node:0.10,0.12,4,5版本 2.更换命名-@babel/xxx 3.移除以年份命名的presets,统一更换成@babel/preset-env 4.移除 ’Stage‘ pr ...

  3. SVN merge(合并) 时看不到以前的已经合并过的记录的标识

    今天遇到这么一个事情,merge的时候以前merge过的提交记录,咩有已合并过的标识了,就是下面这样的尾巴分叉向下的箭头 通常出现这样的情况,都是工程路径不对,检查了一下,没有问题,这些meng B ...

  4. LED Keychain - Widely Used Logo Item

    The LED keychain makes it easy for people to carry their keys with them and carry them with them. It ...

  5. (转)java垃圾回收二

    转自:http://shuaijie506.iteye.com/blog/1779651 在网上看到一篇不错的文章,记录下来备忘. 要理解java对象的生命周期,我们需要要明白两个问题, 1.java ...

  6. rf关键字

    1.获取字典中的key ${b} Set Variable ${a}[0][dealer_buy_price] Log ${b}   2.${b}的float类型转换string 再和后面比较 Sho ...

  7. TCP/IP详解,卷1:协议--第6章 ICMP:Internet控制报文协议

    引言 I C M P经常被认为是 I P层的一个组成部分.它传递差错报文以及其他需要注意的信息. I C M P报文通常被I P层或更高层协议( T C P或U D P)使用.一些I C M P报文把 ...

  8. spring(四):IoC

    IoC-Inversion of Control,即控制反转 IoC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制. 理解IoC的关键:"谁控制谁,控制什么,为何是反 ...

  9. ignoreContentAdaptWithSize

    使用cocosStudio 中 的Text控件时,要设定text.ignoreContentAdaptWithSize(true); 设定控件(0.5,0.5)锚点位置不随文本长度变化而变化.

  10. kali linux2019.4安装启动后中文乱码

    1.鼠标右键找到黑框框打开终端 2.终端执行后重启,乱码解决. sudo apt-get install ttf-wqy-zenhei