RocketMQ-生产者-重试策略

生产者发送消息时,需要进行队列选择,不同的重试策略,选择队列方式不一样。

  • 负载均衡模式:(正常模式)根据队列进行负载,(sendWhichQueueNum++) %(消息队列数量)
  • 故障转移机制: 需要特殊开启,当发现有故障的Broker时,在一定时间内,不选择给Broker下的队列。

1、消息发送代码

private SendResult sendDefaultImpl(
    Message msg,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    this.makeSureStateOK();
    
    // 1、验证消息
    Validators.checkMessage(msg, this.defaultMQProducer);

    final long invokeID = random.nextLong();
    long beginTimestampFirst = System.currentTimeMillis();
    long beginTimestampPrev = beginTimestampFirst;
    long endTimestamp = beginTimestampFirst;
    
    // 2、尝试查找Topic 路由信息:如果本地没有,查询NameServer找到路由信息
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        boolean callTimeout = false;
        MessageQueue mq = null;
        Exception exception = null;
        SendResult sendResult = null;
        int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
        int times = 0;
        String[] brokersSent = new String[timesTotal];
        
        // 3、若是同步消息,需要重试
        for (; times < timesTotal; times++) {
            String lastBrokerName = null == mq ? null : mq.getBrokerName();
            
            // 4、根据上次发送失败的Broker名字,选择Message 的发送Queue
            MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
            if (mqSelected != null) {
                mq = mqSelected;
                brokersSent[times] = mq.getBrokerName();
                try {
                
                // 5、记录发送前时间
                    beginTimestampPrev = System.currentTimeMillis();
                    long costTime = beginTimestampPrev - beginTimestampFirst;
                    if (timeout < costTime) {
                        callTimeout = true;
                        break;
                    }

                // 6、发送消息
                    sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                    
                    // 7、记录消息发送后的时间戳
                    endTimestamp = System.currentTimeMillis();
                    
                    // 8、故障转移机制时生效
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                    
                    
                }
            }
        }
    }
}

2、负载均衡模式

sendLatencyFaultEnablefalse时,走负载均衡模式。

TopicPublishInfo.selectOneMessageQueue

lastBrokerName: 若有重试,lastBorkerName为上次失败的MqName

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    // 1、第一次发送消息,根据自增id%队列数
    if (lastBrokerName == null) {
        return selectOneMessageQueue();
    } else {
        // 2-1、不是第一次发送消息,自增id+1
        int index = this.sendWhichQueue.getAndIncrement();

        // 2-2、获取所有的消息队列
        for (int i = 0; i < this.messageQueueList.size(); i++) {
            int pos = Math.abs(index++) % this.messageQueueList.size();
            if (pos < 0)
                pos = 0;
            MessageQueue mq = this.messageQueueList.get(pos);

            // 2-3、遍历过程中,如果拿到了上次brocker,跳过,换一个broker
            if (!mq.getBrokerName().equals(lastBrokerName)) {
                return mq;
            }
        }

        // 3、自增取模
        return selectOneMessageQueue();
    }
}

public MessageQueue selectOneMessageQueue() {
    int index = this.sendWhichQueue.getAndIncrement();
    int pos = Math.abs(index) % this.messageQueueList.size();
    if (pos < 0)
        pos = 0;
    return this.messageQueueList.get(pos);
}

1、在一次消息发送过程中,可能会多次执行选择消息队列这个方法,lastBrokerName就是上一次选择的执行发送消息失败的Broker。第一次发送是为null

2、第一次执行消息队列选择时,lastBrokerName为null,此时直接用sendWhichQueue自增再获取值,与当前路由表中消息队列个数取模

3、如果消息发送再失败的话,下次进行消息队列选择时规避上次故障的Broker,否则还是很有可能再次失败。

该算法在一次消息发送时,能规避故障的Broker。

2-1、负载均衡模式的问题

考虑以下场景:

该算法在一次消息发送过程中能成功规避故障的Broker,但如果Broker宕机,由于路由算法中的消息队列是按Broker排序的。

如果上一次根据路由算法选择的是宕机的Broker的第一个队列,那么随后的下次选择的是宕机Broker的第二个队列,消息发送很有可能会失败,再次引发重试,带来不必要的性能损耗。

因为每次发送消息,需要通过网络发送给Broker,因此损耗。

3、故障转移机制

该模式引入,就是为了解决负载均衡模式不足的。

MQFaultStrategy:如果能引入一种机制,在Broker宕机期间,如果一次消息发送失败后,可以将该Broker暂时排除在消息队列的选择范围中,就可以避免负载均衡模式的问题。

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    // 1、开启故障转移机制
    if (this.sendLatencyFaultEnable) {
        try {
            int index = tpInfo.getSendWhichQueue().getAndIncrement();
            for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                if (pos < 0)
                    pos = 0;
                MessageQueue mq = tpInfo.getMessageQueueList().get(pos);

                 /**
                 * - null == lastBrokerName,说明没问题,直接根据退出
                 * - broker故障了,但是【可能】已经恢复了。
                 */
                if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                    if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                        return mq;
                }
            }

            final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
            int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
            if (writeQueueNums > 0) {
                final MessageQueue mq = tpInfo.selectOneMessageQueue();
                if (notBestBroker != null) {
                    mq.setBrokerName(notBestBroker);
                    mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                }
                return mq;
            } else {
                latencyFaultTolerance.remove(notBestBroker);
            }
        } catch (Exception e) {
            log.error("Error occurred when selecting message queue", e);
        }
        return tpInfo.selectOneMessageQueue();
    }

    // 2、使用正常模式下的队列选择方式
    return tpInfo.selectOneMessageQueue(lastBrokerName);
}

消息发送端采用重试机制,默认重试两次。

1、在一次消息发送过程中,可能会多次执行选择消息队列这个方法,lastBrokerName就是上一次选择的执行发送消息失败的Broker。

2、第一次执行消息队列选择时,lastBrokerName为null,此时直接用sendWhichQueue自增再获取值,与当前路由表中消息队列个数取模

3、如果消息发送再失败的话,该消息进行重试时,重新选择消息队列选择时规避上次故障的Broker

4、假设有一个BrokerA故障了,还没恢复。准备第二条消息时,进行队列选择,又选择了故障的BrokerA,然后往这个故障的BrokerA发送消息,结果又失败了,然后继续进行重试…。

第一条消息次已经失败了,第二条消息其实可以把BrokerA规避掉的。

RocketMQ的故障转移机制就是在BrokerA 已经故障了,发送不了消息了的时候,给BrokerA一个故障恢复时间, 在这段时间内,发送消息就不选择发送到这台Broker上。

3-1、如何设置这个恢复时间?

 String[] brokersSent = new String[timesTotal];
// 重试
 for (; times < timesTotal; times++) {
    String lastBrokerName = null == mq ? null : mq.getBrokerName();
    MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);

     // 发送前的时间
     beginTimestampPrev = System.currentTimeMillis();
     // 往Broker发发送消息
     sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
     // 发送后的时间,
     endTimestamp = System.currentTimeMillis();
     
     // (endTimestamp - beginTimestampPrev)算出花费时间
     this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);

      // 其他流程
}
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
    // 如果开启了故障转移,才会有下面的逻辑
    if (this.sendLatencyFaultEnable) {
        // 根据特定值,给出一个【延迟时间】
        long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
        this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
    }
}
/**
 *
 * @param name :brokerName
 * @param currentLatency:发送消息到Broker的花费时间,毫秒
 * @param notAvailableDuration:多久不可用时间,根据【经验值】算出来的,毫秒
 */
@Override
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
    // 1、根据BrokerName在Map中查找是否有,若不为空,说明之前就故障过
    FaultItem old = this.faultItemTable.get(name);

    // 2、第一次故障
    if (null == old) {
        final FaultItem faultItem = new FaultItem(name);
        faultItem.setCurrentLatency(currentLatency);

        // 3、开始aviable的时间戳
        faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);

        // 4、返回为空,说明里面没有值
        old = this.faultItemTable.putIfAbsent(name, faultItem);

        // 5、双重检验锁,解决并发问题。不为空,说明被其他线程更新了值。这里需要重新设置
        if (old != null) {
            old.setCurrentLatency(currentLatency);
            old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
        }
    } else {
        // 2、之前故障过,重新设置值
        old.setCurrentLatency(currentLatency);
        old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
    }
}

判断Avaible

@Override
public boolean isAvailable(final String name) {
    final FaultItem faultItem = this.faultItemTable.get(name);
    if (faultItem != null) {
        return faultItem.isAvailable();
    }
    return true;
}

// 当前时间超过延迟级别窗口
public boolean isAvailable() {
    return (System.currentTimeMillis() - startTimestamp) >= 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值