RocketMq系列之topic路由信息更新(二)

1,113 阅读7分钟

欢迎关注公众号【sharedCode】致力于主流中间件的源码分析, 个人网站:www.shared-code.com/

前言

本文的主要是为了介绍,在生产者或者消费者启动的时候,需要实例化MqClientInstance,作为负责和borker通信的实例, 在这个里面初始化了很多定时任务,心跳,nameServer地址更新, topic路由信息更新

源码入口

org.apache.rocketmq.client.impl.factory.MQClientInstance

public void start() throws MQClientException {
        synchronized (this) {
            switch (this.serviceState) {
                case CREATE_JUST:
                    this.serviceState = ServiceState.START_FAILED;
                    // If not specified,looking address from name server
                    if (null == this.clientConfig.getNamesrvAddr()) {
                        this.mQClientAPIImpl.fetchNameServerAddr();
                    }
                    // 负责网络通信的接口调用启动,
                    this.mQClientAPIImpl.start();
                    // 初始化各类定时任务
                    this.startScheduledTask();
                    // 开启pullService , 针对consumer,到讲解consumer时再做详细说明
                    this.pullMessageService.start();
                    // 启动RebalanceService服务,针对consumer,到讲解consumer时再做详细说明
                    this.rebalanceService.start();
                    // 启动一个groupName为CLIENT_INNER_PRODUCER的DefaultMQProducer,
                    // 用于将消费失败的消息发回broker,消息的topic格式为%RETRY%ConsumerGroupName。
                    this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
                    log.info("the client factory [{}] start OK", this.clientId);
                    this.serviceState = ServiceState.RUNNING;
                    break;
                case RUNNING:
                    break;
                case SHUTDOWN_ALREADY:
                    break;
                case START_FAILED:
                    throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
                default:
                    break;
            }
        }
    }

本文主要讲述this.startScheduledTask()方法

private void startScheduledTask() {
        if (null == this.clientConfig.getNamesrvAddr()) {
            // 每两分钟执行一次寻址服务(NameServer地址)
            this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

                @Override
                public void run() {
                    try {
                        MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
                    } catch (Exception e) {
                        log.error("ScheduledTask fetchNameServerAddr exception", e);
                    }
                }
            }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
        }

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
                        // 每30秒更新一次所有的topic的路由信息 , 延迟10毫秒执行
            @Override
            public void run() {
                try {
                    MQClientInstance.this.updateTopicRouteInfoFromNameServer();
                } catch (Exception e) {
                    log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
                }
            }
        }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);
                
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
                        // 定时任务延迟1秒执行
            @Override
            public void run() {
                try {
                    // 每30秒对下线的broker进行移除
                    MQClientInstance.this.cleanOfflineBroker();
                    // 每30秒发送一次心跳
                    MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
                } catch (Exception e) {
                    log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
                }
            }
        }, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
                        // 持久化消费端offSet
            @Override
            public void run() {
                try {
                    MQClientInstance.this.persistAllConsumerOffset();
                } catch (Exception e) {
                    log.error("ScheduledTask persistAllConsumerOffset exception", e);
                }
            }
        }, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    // 调整消费端线程池大小
                    MQClientInstance.this.adjustThreadPool();
                } catch (Exception e) {
                    log.error("ScheduledTask adjustThreadPool exception", e);
                }
            }
        }, 1, 1, TimeUnit.MINUTES);
    }

每两分钟执行一次nameServer寻址

该定时任务仅会在客户端没有主动配置nameServer的情况下会初始化

if (null == this.clientConfig.getNamesrvAddr()) {
            // 每两分钟执行一次寻址服务(NameServer地址)
            this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

                @Override
                public void run() {
                    try {
                        MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
                    } catch (Exception e) {
                        log.error("ScheduledTask fetchNameServerAddr exception", e);
                    }
                }
            }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
        }

定义了一个定时线程池, 延迟10秒钟触发, 每次执行间隔时间为 2分钟

public String fetchNameServerAddr() {
        try {
            // 远程服务获取
            String addrs = this.topAddressing.fetchNSAddr();
            if (addrs != null) {
                if (!addrs.equals(this.nameSrvAddr)) {
                    log.info("name server address changed, old=" + this.nameSrvAddr + ", new=" + addrs);
                    // 更新本地的nameAddressList
                    this.updateNameServerAddressList(addrs);
                    this.nameSrvAddr = addrs;
                    return nameSrvAddr;
                }
            }
        } catch (Exception e) {
            log.error("fetchNameServerAddr Exception", e);
        }
        return nameSrvAddr;
    }

该功能可以理解为,rocketMq里面其实是可以不配置nameSrvAddr的,支持动态获取, 程序启动的时候自动获取一次, 此后通过定时任务每两分钟进行更新一次, 但是笔者通过源码看了一下,目前的版本并不支持动态配置获取nameServer地址的接口, 默认写死了这个地址:http://jmenv.tbsite.net:8080/rocketmq/nsaddr , 大家有兴趣,改改源码就可以用了。 功能天然支持

每30秒更新一次所有的topic的路由信息

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
                        // 每30秒更新一次所有的topic的路由信息 , 延迟10毫秒执行
            @Override
            public void run() {
                try {
                    MQClientInstance.this.updateTopicRouteInfoFromNameServer();
                } catch (Exception e) {
                    log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
                }
            }
}, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);

延迟10毫秒执行,this.clientConfig.getPollNameServerInterval() 默认值为 1000 * 30 , 每30秒执行一次

    private int pollNameServerInterval = 1000 * 30;
updateTopicRouteInfoFromNameServer
public void updateTopicRouteInfoFromNameServer() {
        Set<String> topicList = new HashSet<String>();

        // Consumer , 消费者
        {
            Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
            while (it.hasNext()) {
                Entry<String, MQConsumerInner> entry = it.next();
                MQConsumerInner impl = entry.getValue();
                if (impl != null) {
                    // 获取消费者订阅了的topic 
                    Set<SubscriptionData> subList = impl.subscriptions();
                    if (subList != null) {
                        for (SubscriptionData subData : subList) {
                            topicList.add(subData.getTopic());
                        }
                    }
                }
            }
        }

        // Producer , 生产者
        {
            // 获取当前系统中生产者的列表
            Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
            // 循环
            while (it.hasNext()) {
                // 获取生产者信息
                Entry<String, MQProducerInner> entry = it.next();
                MQProducerInner impl = entry.getValue();
                if (impl != null) {
                    // 获取最新的topic信息。放到Set集合里面
                    Set<String> lst = impl.getPublishTopicList();
                    topicList.addAll(lst);
                }
            }
        }
            // 获取到了所有的topic信息,
        for (String topic : topicList) {
            // 分批次的循环更新topic信息
            this.updateTopicRouteInfoFromNameServer(topic);
        }
    }

producerTable 在生产者启动的时候,会调用registerProducer方法将自身的信息注册信息。

MQProducerInner 的实现类是DefaultMQProducerImpl , 每个DefaultMQProducerImpl里面都会维护了一个topicPublishInfoTable, 是一个Map类型的内部变量, 负责存储当前DefaultMQProducerImpl关注的topic信息。 下面可以看一下getPublishTopicList的实现

@Override
    public Set<String> getPublishTopicList() {
        Set<String> topicList = new HashSet<String>();
        for (String key : this.topicPublishInfoTable.keySet()) {
            topicList.add(key);
        }

        return topicList;
    }

上面的代码不做过多的解释,其实就是把生产者内部维护的topic信息 ,放入到Set然后返回

---------------------------------------------------划重点----------------------------- --------------------------------------------------

上面的代码虽然写的很多,但是本质上就是两点

  1. 获取消费者(订阅),生产者(发布)的所有topic, 一个收集topic的动作
  2. 获取到topic之后,进行循环更新,调用updateTopicRouteInfoFromNameServer方法

下面就是本文的重点了

updateTopicRouteInfoFromNameServer实现

该方法属于topic信息更新的必经之路, 重点看一波

public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
                                                      DefaultMQProducer defaultMQProducer) {
        try {   
            // 先上一把锁
            if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
                try {
                    // topic路由信息
                    TopicRouteData topicRouteData;
                    if (isDefault && defaultMQProducer != null) {
                        // 1.  进入此段代码块,仅是在通过默认的Topic,默认的producucer去nameServer获取路由信息
                        topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
                                1000 * 3);
                        // topicRouteData 这个里面包含了默认的createTopicKey这个topic的信息,主要是为了后续构建不存在topic
                        if (topicRouteData != null) {
                            for (QueueData data : topicRouteData.getQueueDatas()) {
                                // 设置topic队列数, 默认为4个
                                int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
                              // 设置读队列
                                data.setReadQueueNums(queueNums);
                                // 写队列
                                data.setWriteQueueNums(queueNums);
                            }
                        }
                    } else {
                       // 2. 除了默认情况,其他情况,就是实实在在的去nameServer获取当前topic的信息
                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
                    }
                    // 当topic信息不为空的时候
                    if (topicRouteData != null) {
                        // 获取原先存在topicRouteTable中的老的topic,如果是第一次,则为空
                        TopicRouteData old = this.topicRouteTable.get(topic);
                        // 判断新老topic信息是否一致,是否需要更新
                        boolean changed = topicRouteDataIsChange(old, topicRouteData);
                        if (!changed) {
                            // 当比较新老信息是一致的时候,这个方法回去判断 topic在本地是否存在,不存在还是属于需要更新
                            changed = this.isNeedUpdateTopicRouteInfo(topic);
                        } else {
                            log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
                        }

                        if (changed) {
                            // 克隆一个TopicRouteData
                            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
                                                        // 获取当前topic存在的borker集合
                            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                                // 收集broker地址
                                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
                            }

                            // Update Pub info
                            {   // 更新发布的topic, 针对生产者
                                TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);                            // 设置拥有topic信息
                                publishInfo.setHaveTopicRouterInfo(true);
                                Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQProducerInner> entry = it.next();
                                    MQProducerInner impl = entry.getValue();
                                    if (impl != null) {
                                        // 更新topic信息
                                        impl.updateTopicPublishInfo(topic, publishInfo);
                                    }
                                }
                            }

                            // Update sub info
                            {
                                // 更新订阅的topic, 针对消费者
                                Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
                                Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQConsumerInner> entry = it.next();
                                    MQConsumerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);
                                    }
                                }
                            }
                            log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);
                            this.topicRouteTable.put(topic, cloneTopicRouteData);
                            return true;
                        }
                    } else {
                        log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}", topic);
                    }
                } catch (Exception e) {
                    // 当topic不存在时,会被异常catch住,但是异常并不打印
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX) && !topic.equals(MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)) {
                        log.warn("updateTopicRouteInfoFromNameServer Exception", e);
                    }
                } finally {
                    this.lockNamesrv.unlock();
                }
            } else {
                log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms", LOCK_TIMEOUT_MILLIS);
            }
        } catch (InterruptedException e) {
            log.warn("updateTopicRouteInfoFromNameServer Exception", e);
        }

        return false;
    }
getTopicRouteInfoFromNameServer

调用remotingClient获取远程的接口,从nameServer上获取信息。

public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis,
        boolean allowTopicNotExist) throws MQClientException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException {
        GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader();
        requestHeader.setTopic(topic);

        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINTO_BY_TOPIC, requestHeader);
                // 调用原创接口
        RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis);
        assert response != null;
        switch (response.getCode()) {
            // topic不存在
            case ResponseCode.TOPIC_NOT_EXIST: {
                // 是否允许topic不存在 && topic不等于TBW102
                if (allowTopicNotExist && !topic.equals(MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)) {
                    log.warn("get Topic [{}] RouteInfoFromNameServer is not exist value", topic);
                }

                break;
            }
            // topic存在
            case ResponseCode.SUCCESS: {
                byte[] body = response.getBody();
                if (body != null) {
                    return TopicRouteData.decode(body, TopicRouteData.class);
                }
            }
            default:
                break;
        }
                // 抛个异常,被最外围的程序catch住
        throw new MQClientException(response.getCode(), response.getRemark());
    }

总结:

方法updateTopicRouteInfoFromNameServer总共三个地方会被调用

  1. 当进行消息发布的是,有可能会发生两次调用,需要获取topic信息的时候回去 , 源码如下
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
                // 本地获取topic信息
        TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
        // 本地不存在
        if (null == topicPublishInfo || !topicPublishInfo.ok()) {
                // 构建一个空的topic信息放入本地内存
            this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
            // 调用更新topic信息的方法,就是上面全篇讲解的方法
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
        }
                // topic是否拥有正确的路由信息 || 当前topic的队列信息是否正常
        if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
            return topicPublishInfo;
        } else {
                // 当以上都走不通,也就是topic不存在的时候。走默认的topic更新方法,
                // 主要是为了获取topic的信息,queue, filter, borkerNodes
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
            return topicPublishInfo;
        }
    }

上面有两个地方会调用到updateTopicRouteInfoFromNameServer

​ 第一个地方是首次使用这个topic时,本地的topic信息不存在,需要正常的去nameServer中获取

​ 第二个地方是在不能正常的获取topic的时候(一般是topic不存在),需要走updateTopicRouteInfoFromNameServer的默认调用。

  1. 第三个地方调用就是定时任务调用,用来更新本地的topic信息,但是一会不走默认的逻辑
/**
 * 1. 发送消息时,获取topic时调用
 * 2. 定时任务,每30秒获取一次
 */
public boolean updateTopicRouteInfoFromNameServer(final String topic) {
        return updateTopicRouteInfoFromNameServer(topic, false, null);
    };
/**
 * 当topic不存在时,会直接调用该方法,同时isDefault = true, defaultMQProducer!=null 
 */
public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
                                                      DefaultMQProducer defaultMQProducer)

总结: 其实说到这里,大家可能会updateTopicRouteInfoFromNameServer方法中isDefault的参数有点不理解,一张图解释

在这里插入图片描述

注: topic是否支持自动创建,是在发送消息的时候,broker根据autoCreateTopicEnable的属性来判断是否允许