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

问题

  • 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. 多模块打war包

    1.在启动类的那个模块中的pom.xml中加入<packaging>war</packaging>  就这句 <groupId>com.mybatis</gr ...

  2. Python-selenium,使用SenKey模块时所碰到的坑

    一.SenKey模块(模拟鼠标键盘操作) :python3中没有该模块,使用PyUserInput模块代替 二.PyUserInput模块安装前需要安装:pywin32和pyHook模块,pywin3 ...

  3. ASP.NET MVC模型绑定1

    一.模型绑定原理 模型绑定是指为Controller的Action方法的参数提供值的过程,例如我有一个名为Blog的实体类(准确的说是ViewModel),它有一个名为Title的属性,如果我在VIE ...

  4. jenkins 集成环境搭建

    http://www.cnblogs.com/jenniferhuang/p/3355252.html

  5. 手动添加ubuntu服务

    在/etc/init.d/目录下创建一个简单的服务脚本,假设脚本名为hello #!/bin/sh case "$1" in start) # start 的代码 ;; stop) ...

  6. unity的一些特殊目录

    Hidden Folders Folders that start with a dot (e.g. ".UnitTests/", ".svn/") are i ...

  7. MySQL转译

    /* 案例3:查询员工名中第二个字符为 _ 的员工名 */ SELECT last_name, salary FROM employees WHERE last_name LIKE '_$_%' ES ...

  8. 什么是类的hashcode值

    1.要知道什么是类的hashcode值,首要要了解什么是hash(哈希).Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换 ...

  9. 设置canvas的背景成白色

    解决方案一:将透明的pixel设成白色 因为png图片的背景都是透明的,所以我们可以寻找透明的pixel,然后将其全部设置成白色,核心代码如下: JavaScript Code复制内容到剪贴板 // ...

  10. nginx知识学习

    设备: macbook 有用的命令行: sudo nginx -t  测试nginx是否正常 sudo nginx -s reload  平滑重启 配置目录: /usr/local/etc/nginx ...