RocketMQ进阶(四)

本文深入探讨RocketMQ的生产者和消费者消息重试机制,包括同步与异步消息的重试策略、消息重复的可能性及如何配置重试次数。在消费者端,详细解析了顺序消息和无序消息的重试流程,以及服务端如何控制重试。通过对源码的分析,揭示RocketMQ消息重试的底层实现。
摘要由CSDN通过智能技术生成

今天的博客主题

      MQ消息中间件 --》RocketMQ --》RocketMQ进阶(四)


本文主要讲解RocketMQ生产者消费者的消息重试机制及底层实现原理刨根问底

 

生产者消息重试

 

生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway没有任何保证。

消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题。

消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、消费者也会导致重复消费消息。

如下方法可以设置消息重试策略:

retryTimesWhenSendFailed:同步发送失败重投次数,默认为2。生产者会最多尝试发送retryTimesWhenSendFailed + 1次。不会选择上次失败的broker,向其他broker发送,最大程度保证消息不丢。

超过重投次数,抛出异常,由客户端保证消息不丢。当出现RemotingException、MQClientException和部分MQBrokerException时会重投。

retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢。

retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分的重要消息可以开启。

 

DefaultMQProducer 属性可以设置,默认是2,但是重试机制是关闭的,没有开启。

// 在同步模式下发送失败在内部执行的最大重试次数。 可能会导致消息重复,这由应用程序开发人员来解决。
private int retryTimesWhenSendFailed = 2;
// 在异步模式下发送失败在内部执行的最大重试次数。 可能会导致消息重复,这由应用程序开发人员来解决。
private int retryTimesWhenSendAsyncFailed = 2;
// 在内部发送失败时是否重试其他代理。
private boolean retryAnotherBrokerWhenNotStoreOK = false;

在这个里面提供的属性只要有 getter/setter 方法 都可以去调试更改默认值。

我这边用是rocketmq-spring-boor-starter 只要在配置文件增加配置即可

rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    # 开启内部消息重试
    retry-next-server: true

DefaultMQProducerImpl.sendDefaultImpl(); 生产者发送消息实现

private SendResult sendDefaultImpl(){
    // 部分代码省略
    // ...
    // 消息发送次数
    int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
    int times = 0;
    String[] brokersSent = new String[timesTotal];
    for (; times < timesTotal; times++) {
        // 发送消息核心实现
        sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
        switch (communicationMode) {
            case ASYNC:
                return null;
            case ONEWAY:
                return null;
            case SYNC:
                // 同步发送 返回结果不是SEND_OK
                if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
                    // 发送失败是否重试其他代理(注意:默认未开启)
                    if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
                        // 跳过当前循环发送,进行下一次发送
                        continue;
                    }
                }
                return sendResult;
            default:
                break;
        }
        // 这个发送是被try{}catch(){}了的
        // 如果发生了 RemotingException MQClientException MQBrokerException 异常时候都会被重试
    }
}

这里就是生产者同步发送消息失败消息重投实现。

实现思路挺简单的,就是简单一循环,只要服务端不给我 SEND_OK 就循环投递消息给服务端

 

消费者消息重试

 

消费者消费消息失败后,提供一种重试机制,可以让消息再重新消费一次。

 

顺序消息的重试:对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。

因此,建议使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。

无序消息的重试:对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,可以通过设置返回状态达到消息重试的结果。

无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。

重试次数:消息队列 RocketMQ 默认允许每条消息最多重试 16 次。如果消息重试 16 次后仍然失败,消息将不再投递。

会在 4 小时 46 分钟之内进行 16 次重试。按照时间衰减策略执行。

一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。

消费者的重试是有服务端来控制发起的。其现原理和延迟消息的发送是一样的。

那这个消费者的重试配置就在 broker.conf 或 broker.properties 的服务端配置文件里了。

 

先看客户端对消费者的实现:

DefaultMQPushConsumerImpl.start() 在 《RocketMQ进阶(二)》里有详细介绍,在这就不啰嗦了。

在这个默认消费者启动方法里会有一个 mQClientFactory.start(); 客户端工厂的启动,工厂在启动的时候启动了执行了 this.pullMessageService.start();

PullMessageService 继承了 ServiceThread 线程类,PullMessageService里的run方法就是所执行的 start()

@Override
public void run() {
    log.info(this.getServiceName() + " service started");
    // 线程是否被停止了,线程存在一直循环拉取
    while (!this.isStopped()) {
        try {
            // 从拉取请求队列里获取拉取请求上下文
            PullRequest pullRequest = this.pullRequestQueue.take();
            // 拉取消息
            this.pullMessage(pullRequest);
        } catch (InterruptedException ignored) {
        } catch (Exception e) {
            log.error("Pull Message Service Run Method exception", e);
        }
    }
    log.info(this.getServiceName() + " service end");
}

this.pullMessage(pullRequest);

private void pullMessage(final PullRequest pullRequest) {
    // 从消费者列表获取消费者信息
    final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
    // 消费者存在,拉取消息
    if (consumer != null) {
        DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
        impl.pullMessage(pullRequest);
    } else {
        log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
    }
}

impl.pullMessage(pullRequest);

public void pullMessage(final PullRequest pullRequest) {
    // 从拉取请求获取消费队列快照
    final ProcessQueue processQueue = pullRequest.getProcessQueue();
    // 请求是否被抛弃
    if (processQueue.isDropped()) {
        log.info("the pull request[{}] is dropped.", pullRequest.toString());
        return;
    }
    // 最后一次拉取时间戳
    pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
    // 确保服务正在运行,否则存储到稍后执行拉取请求队列中
    try {
        this.makeSureStateOK();
    } catch (MQClientException e) {
        log.warn("pullMessage exception, consumer state not ok", e);
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
        return;
    }
    // 消费是否被暂停,否则存储到稍后执行拉取请求队列中
    if (this.isPause()) {
        log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
        return;
    }
    // 缓存消息总数
    long cachedMessageCount = processQueue.getMsgCount().get();
    // 缓存消息大小
    long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
    // 缓存消息总数是否超过阈值,否则存储到稍后执行拉取请求队列中
    if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
        if ((queueFlowControlTimes++ % 1000) == 0) {
            log.warn(
                "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
                this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
        }
        return;
    }
    // 缓存消息大小是否超过阈值,否则存储到稍后执行拉取请求队列中
    if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
        if ((queueFlowControlTimes++ % 1000) == 0) {
            log.warn(
                "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
                this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
        }
        return;
    }
    // 是否顺序消费
    if (!this.consumeOrderly) { // 并发消费
        // 是否超过并发最大偏移量,否则存储到稍后执行拉取请求队列中
        if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()) {
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
            if ((queueMaxSpanFlowControlTimes++ % 1000) == 0) {
                log.warn(
                    "the queue's messages, span too long, so do flow control, minOffset={}, maxOffset={}, maxSpan={}, pullRequest={}, flowControlTimes={}",
                    processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), processQueue.getMaxSpan(),
                    pullRequest, queueMaxSpanFlowControlTimes);
            }
            return;
        }
    } else { // 顺序消费
        if (processQueue.isLocked()) {
            // 顺序拉取消息,没有被锁定
            if (!pullRequest.isLockedFirst()) {
                // 计算从什么位置拉取
                final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
                // 计算出的拉取位置 小于 请求下一个偏移量  则代理忙
              
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值