RocketMq系列之消息重试及死信队列(十)

推荐关注公众号: sharedCode, 在这里可以直接联系我哦。

前言

上文中我们介绍的客户端普通消息和顺序消息的原理,在消息发送失败的时候会有一个重试的过程,接下来我们来看下消息重试的源码是什么样子的,以及重试到了最后是在什么情况下进入死信队列的

sendMessageBack

public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName)
        throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
        try {
            // 获取broker地址
            String brokerAddr = (null != brokerName) ? this.mQClientFactory.findBrokerAddressInPublish(brokerName)
                : RemotingHelper.parseSocketAddressAddr(msg.getStoreHost());
            // 发起请求
            this.mQClientFactory.getMQClientAPIImpl().consumerSendMessageBack(brokerAddr, msg,
                this.defaultMQPushConsumer.getConsumerGroup(), delayLevel, 5000, getMaxReconsumeTimes());
        } catch (Exception e) {
            log.error("sendMessageBack Exception, " + this.defaultMQPushConsumer.getConsumerGroup(), e);
            // 当发送失败的时候,继续重试。
            Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody());
            String originMsgId = MessageAccessor.getOriginMessageId(msg);
            MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId);
            newMsg.setFlag(msg.getFlag());
            MessageAccessor.setProperties(newMsg, msg.getProperties());
            MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic());
            MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes() + 1));
            MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes()));
            // 重置延迟级别
            newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes());
            // 调用消息发送接口进行发送
            this.mQClientFactory.getDefaultMQProducer().send(newMsg);
        } finally {
            msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace()));
        }
    }

步骤说明:

  1. 获取borker地址,调用mQClientFactory 进行发送
  2. 发送失败的时候,组装消息继续发送,这个原生的send接口默认有三次重试

最大重试次数

private int getMaxReconsumeTimes() {
        // default reconsume times: 16
        if (this.defaultMQPushConsumer.getMaxReconsumeTimes() == -1) {
            return 16;
        } else {
            return this.defaultMQPushConsumer.getMaxReconsumeTimes();
        }
    }

发送重试消息的时候,最大重试次数是根据我们客户端配置的,默认配置是 -1 ,所以得到最大重试次数是 16 , 其实相当于重试16次之后默认会进入死信队列 , 当然客户端也可以自定义

public void consumerSendMessageBack(
        final String addr,
        final MessageExt msg,
        final String consumerGroup,
        final int delayLevel,
        final long timeoutMillis,
        final int maxConsumeRetryTimes
    ) throws RemotingException, MQBrokerException, InterruptedException {
        ConsumerSendMsgBackRequestHeader requestHeader = new ConsumerSendMsgBackRequestHeader();
  			// 注意这里,构建请求的命令
        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.CONSUMER_SEND_MSG_BACK, requestHeader);

        // 设置消费组
        requestHeader.setGroup(consumerGroup);
        // 设置topic
        requestHeader.setOriginTopic(msg.getTopic());
  			// 设置当前需要重试消息的offset
        requestHeader.setOffset(msg.getCommitLogOffset());
        // 设置消息级别
        requestHeader.setDelayLevel(delayLevel);
        requestHeader.setOriginMsgId(msg.getMsgId());
        //设置最高可重试的次数
        requestHeader.setMaxReconsumeTimes(maxConsumeRetryTimes);

  		  // 执行远程方法
        RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),
            request, timeoutMillis);
        assert response != null;
        switch (response.getCode()) {
            case ResponseCode.SUCCESS: {
                return;
            }
            default:
                break;
        }

        throw new MQBrokerException(response.getCode(), response.getRemark());
    }

步骤说明:

  1. 构建请求的RemotingCommand ,这个很重要,是我们如何找到broker代码的关键,这里有个RequestCode.CONSUMER_SEND_MSG_BACK, 通过这个值我们可以找到broker的处理代码,因为在broker的原理里面,是通过RequestCode来进行switch选择的。
  2. 从上面可以看出来,整个的消息发送过去,是没有把实际的消息内容重新发送回去borker, 而是通过旧消息的offset的去broker上面去寻找消息的,这点在后面的broker的源码上会讲解。
  3. 执行远程的方法,发送命令给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;
                }
                System.out.println(JSON.toJSONString(requestHeader));
                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;
        }
    }

说明:

从上面我们可以看到,这个是broker的消息处理,switch就两个选择,一个是重试消息的,一个是正常消息的处理,正常消息的处理后面我们会单独开文讲解,本文仅讲解重试消息

consumerSendMsgBack
private RemotingCommand consumerSendMsgBack(final ChannelHandlerContext ctx, final RemotingCommand request)
        throws RemotingCommandException {
        // 构建响应的Response
        final RemotingCommand response = RemotingCommand.createResponseCommand(null);
        // 获取请求header
        final ConsumerSendMsgBackRequestHeader requestHeader =
            (ConsumerSendMsgBackRequestHeader)request.decodeCommandCustomHeader(ConsumerSendMsgBackRequestHeader.class);

        // 去除%DLQ%(死信) ,%RETRY%"(重试) 这两种消息消费组的前缀
        String namespace = NamespaceUtil.getNamespaceFromResource(requestHeader.getGroup());
        // 执行hook接口
        if (this.hasConsumeMessageHook() && !UtilAll.isBlank(requestHeader.getOriginMsgId())) {

            ConsumeMessageContext context = new ConsumeMessageContext();
            context.setNamespace(namespace);
            context.setConsumerGroup(requestHeader.getGroup());
            context.setTopic(requestHeader.getOriginTopic());
            context.setCommercialRcvStats(BrokerStatsManager.StatsType.SEND_BACK);
            context.setCommercialRcvTimes(1);
            context.setCommercialOwner(request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER));

            this.executeConsumeMessageHookAfter(context);
        }
        // 根据消费组名称,获取订阅组配置
        SubscriptionGroupConfig subscriptionGroupConfig =
            this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(requestHeader.getGroup());
        if (null == subscriptionGroupConfig) {
            // 订阅组不存在
            response.setCode(ResponseCode.SUBSCRIPTION_GROUP_NOT_EXIST);
            response.setRemark("subscription group not exist, " + requestHeader.getGroup() + " "
                + FAQUrl.suggestTodo(FAQUrl.SUBSCRIPTION_GROUP_NOT_EXIST));
            return response;
        }

        // 判断broker是否有写的全新,
        if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())) {
            response.setCode(ResponseCode.NO_PERMISSION);
            response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending message is forbidden");
            return response;
        }
        // 消费组的重试队列数量为0
        if (subscriptionGroupConfig.getRetryQueueNums() <= 0) {
            response.setCode(ResponseCode.SUCCESS);
            response.setRemark(null);
            return response;
        }
        // 发送消息的TOPIC, 重新设置TOPIC,前面加上 RETRY
        String newTopic = MixAll.getRetryTopic(requestHeader.getGroup());
        // 随机重试队列
        int queueIdInt = Math.abs(this.random.nextInt() % 99999999) % subscriptionGroupConfig.getRetryQueueNums();

        int topicSysFlag = 0;
        if (requestHeader.isUnitMode()) {
            topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
        }
        // 负责获取并创建topicConfig (不存在就创建)
        TopicConfig topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
            newTopic,
            subscriptionGroupConfig.getRetryQueueNums(),
            PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag);
        if (null == topicConfig) {
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("topic[" + newTopic + "] not exist");
            return response;
        }

        if (!PermName.isWriteable(topicConfig.getPerm())) {
            response.setCode(ResponseCode.NO_PERMISSION);
            response.setRemark(String.format("the topic[%s] sending message is forbidden", newTopic));
            return response;
        }
        // 通过原消息的offset获取消息实际的内容。
        MessageExt msgExt = this.brokerController.getMessageStore().lookMessageByOffset(requestHeader.getOffset());
        if (null == msgExt) {
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("look message by offset failed, " + requestHeader.getOffset());
            return response;
        }
        // 设置重试TOPIC
        final String retryTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
        if (null == retryTopic) {
            MessageAccessor.putProperty(msgExt, MessageConst.PROPERTY_RETRY_TOPIC, msgExt.getTopic());
        }
        msgExt.setWaitStoreMsgOK(false);

        int delayLevel = requestHeader.getDelayLevel();
        // 获取最大消息重试次数
        int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes();
        if (request.getVersion() >= MQVersion.Version.V3_4_9.ordinal()) {
            // RocketMq 3.4.9之后以客户端的配置为准
            maxReconsumeTimes = requestHeader.getMaxReconsumeTimes();
        }

        // 当消息的重试次数大于 规定的次数, 或者重试等级小于0
        if (msgExt.getReconsumeTimes() >= maxReconsumeTimes
            || delayLevel < 0) {
            // 设置新的TOPIC, %DLQ%+ 实际的消费组
            newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
            // 随机队列
            queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP;

            //
            topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
                DLQ_NUMS_PER_GROUP,
                PermName.PERM_WRITE, 0
            );
            if (null == topicConfig) {
                response.setCode(ResponseCode.SYSTEM_ERROR);
                response.setRemark("topic[" + newTopic + "] not exist");
                return response;
            }
        } else {
            if (0 == delayLevel) {
                delayLevel = 3 + msgExt.getReconsumeTimes();
            }

            msgExt.setDelayTimeLevel(delayLevel);
        }

        // 发送消息
        MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
        msgInner.setTopic(newTopic);
        msgInner.setBody(msgExt.getBody());
        msgInner.setFlag(msgExt.getFlag());
        MessageAccessor.setProperties(msgInner, msgExt.getProperties());
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
        msgInner.setTagsCode(MessageExtBrokerInner.tagsString2tagsCode(null, msgExt.getTags()));

        msgInner.setQueueId(queueIdInt);
        msgInner.setSysFlag(msgExt.getSysFlag());
        msgInner.setBornTimestamp(msgExt.getBornTimestamp());
        msgInner.setBornHost(msgExt.getBornHost());
        msgInner.setStoreHost(this.getStoreHost());
        msgInner.setReconsumeTimes(msgExt.getReconsumeTimes() + 1);

        String originMsgId = MessageAccessor.getOriginMessageId(msgExt);
        MessageAccessor.setOriginMessageId(msgInner, UtilAll.isBlank(originMsgId) ? msgExt.getMsgId() : originMsgId);
        // 消息持久化,持久化这一块的代码后续我们会继续讲解
        PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner);
        if (putMessageResult != null) {
            switch (putMessageResult.getPutMessageStatus()) {
                case PUT_OK:
                    // 发送完成
                    String backTopic = msgExt.getTopic();
                    String correctTopic = msgExt.getProperty(MessageConst.PROPERTY_RETRY_TOPIC);
                    if (correctTopic != null) {
                        backTopic = correctTopic;
                    }
                    // 统计
                    this.brokerController.getBrokerStatsManager().incSendBackNums(requestHeader.getGroup(), backTopic);

                    response.setCode(ResponseCode.SUCCESS);
                    response.setRemark(null);

                    return response;
                default:
                    break;
            }

            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark(putMessageResult.getPutMessageStatus().name());
            return response;
        }

        response.setCode(ResponseCode.SYSTEM_ERROR);
        response.setRemark("putMessageResult is null");
        return response;
    }

步骤说明:

  1. 获取消费组,重试队列等准备信息,这个不做过多说明,此处不多说

  2. 判断broker的写权限,这个在broker端的配置文件中有配置,brokerPermission 6 : 同时支持读写 4 : 禁写 2 禁读

  3. 设置重试消息的TOPIC, 重试队列名称为:%RETRY%+consumergroup , 负责获取并创建topicConfig (不存在就创建)

  4. topic是否支持写,这个都会做好判断

    在这里插入图片描述
    这里跟broker一样,6 : 同时支持读写 4 : 禁写 2 禁读

  5. 通过原消息的offset获取消息内容,这个在客户端发送的时候说过,这样做可以极大的减少消息传输量​

  6. 判断重试次数,RocketMq3.4.9版本之后的最大重试次数以客户端的为主 , 如果当前消息的重试次数大于最大重试次数,那么就开始走死信队列。 跟重试消息一样,也是设置死信队列的TOPIC %DLQ%+ 实际的消费组 ,

  7. 以上步骤走完之后,需要走重试topic还是走死信topic这个已经确定好了,

  8. 接下来就是走发送消息的流程了。

这里需要注意的是,死信队列的perm设置是 0x1 << 1 也就是 2 , 2 的意思就是说死信队列可写不可读

因此死信队列在控制台上查询,默认是查询不到的

在这里插入图片描述

如果需要查询的话,那么需要在topic的配置里面对topic 的权限进行修改一下,就可以查看到了

在这里插入图片描述

推荐关注公众号: sharedCode, 在这里可以直接联系我哦。

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值