RocketMQ topic路由

原创文章,转载请注明出处:http://jameswxx.iteye.com/blog/2096446

这里以消费者为例说明。一组消费者要消费某个topic,得先知道该topic分布在哪些broker上,某个broker上的topic分布可能会变化,一旦变化,生产者和消费者应该都能被通知到。通知模式有推和拉两种,客户端都是采取拉的模式,所以broker如有变化,通知都是有延迟的。

 

一 什么时候启动topic路由获取任务

两个地方:

1 首先是DefaultMQPushConsumerImpl启动时,见DefaultMQPushConsumerImpl的start方法里的this .updateTopicSubscribeInfoWhenSubscriptionChanged();

2 另外DefaultMQPushConsumerImpl的start方法也启动了MQClientInstance,MQClientInstance的start方法里调用了startScheduledTask()方法,该方法启动了获取路由的定时任务。

        // 定时从Name Server获取Topic路由信息

        this.scheduledExecutorService .scheduleAtFixedRate(new Runnable() {

            @Override

            public void run() {

                try {

                    MQClientInstance.this .updateTopicRouteInfoFromNameServer();

                }

                catch (Exception e) {

                    log.error( "ScheduledTask updateTopicRouteInfoFromNameServer exception", e);

                }

            }

        }, 10, this.clientConfig .getPollNameServerInteval(), TimeUnit.MILLISECONDS );

 

 

二 每隔多久获取一次

很简单,看定时任务每隔多久执行一次就知道了,这里的间隔参数是this.clientConfig .getPollNameServerInteval()。

ClientConfig的pollNameServerInteval 定义如下:

private int pollNameServerInteval = 1000 * 30;

DefaultMQPushConsumer继承了ClientConfig,pollNameServerInteval 默认是30秒,显然,这个时间是可以自己定义的,通过DefaultMQPushConsumer的setPollNameServerInteval()方法。

 

三 获取路由过程

看MQClientInstance的updateTopicRouteInfoFromNameServer()方法,该方法最终会调用下面这个方法,需要注意,对于消费者而言,isDefault参数永远是false。

  public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,DefaultMQProducer defaultMQProducer) {

        try {

            if (this.lockNamesrv .tryLock(LockTimeoutMillis, TimeUnit.MILLISECONDS )) {

                try {

                    TopicRouteData topicRouteData;

                    if (isDefault && defaultMQProducer != null) {

                       //此处省略不必要的信息,对于消费者,分支不会走到这里来,因为isDefault为false,且生产者肯定为空

                    }

                    else {

                        topicRouteData =

                                this.mQClientAPIImpl .getTopicRouteInfoFromNameServer(topic, 1000 * 3);

                    }

                    //此处省略无关语句

                }

                catch (Exception e) {

                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX )

                            && !topic.equals(MixAll.DEFAULT_TOPIC )) {

                        log.warn("updateTopicRouteInfoFromNameServer Exception" , e);

                    }

                }

                finally {

                    this.lockNamesrv .unlock();

                }

            }

            else {

                log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms"LockTimeoutMillis);

            }

        }

        catch (InterruptedException e) {

            log.warn( "updateTopicRouteInfoFromNameServer Exception", e);

        }

 

        return false ;

  }

 

其实最终都是通过this .mQClientAPIImpl .getTopicRouteInfoFromNameServer(topic, 1000 * 3);得到的。

 

 

 

四 客户端与nameserver的连接关系

broker与所有nameserver都是长连接,如有变化,则向所有nameserver都发送消息。但是生产者和消费者只是跟某一台nameserver保持联系。设定一个场景,如果某个broker的topic配置发生了变化,它向所有nameserver发布通知,但是此时如果某一台nameserver推送失败(超时或者挂掉了),则nameserver集群之间的信息是不完整的,因为挂掉的那台nameserver没有得到最新变化。

由此衍生三个问题:

1 如果该nameserver不是挂掉,只是那一瞬间没有响应,那么待可正常服务时,刚才那个borker发生的变化应该能生效,不应该被丢弃,否则nameserver之间的数据是不同步的。

  解决方案:broker是定时向所有nameserver发送自己的注册信息的,如果当时某台nameserver挂掉重启或者超时,没关系,下次仍然会接受到上次没接收到的broker信息

2 如果真的挂掉了,但是很快又恢复了,因为borker和nameserver保持的是长连接,显然挂掉重新启动后,broker与nameserver的长连接无效了,应该能自动重连

  getAndCreateChannel方法分析

3 只要某个nameserver不可用,消费者应该能failover,每次应该都检查长连接是否还有效,若无效则自动连接其他nameserver。

  getAndCreateNameserverChannel()方法分析

 

带着这个疑问,看看this .mQClientAPIImpl .getTopicRouteInfoFromNameServer(topic, 1000 * 3)方法。这个方法向nameserver发起调用,获取路由结果

RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode. GET_ALL_TOPIC_LIST_FROM_NAMESERVER , null);

RemotingCommand response = this .remotingClient .invokeSync( null, request, timeoutMillis);

 

重点在于remotingClient .invokeSync方法,如下

@Override

    public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)

            throws InterruptedException, RemotingConnectException, RemotingSendRequestException,

            RemotingTimeoutException {

        //这里获取连接,该方法里面会做连接的检查和恢复

        final Channel channel = this .getAndCreateChannel(addr);

 

        //最后如果还是不是有效连接,则关闭连接,抛出异常

        if (channel != null && channel.isActive()) {

            try {

                if (this .rpcHook != null) {

                    this .rpcHook .doBeforeRequest(addr, request);

                }

                RemotingCommand response = this .invokeSyncImpl(channel, request, timeoutMillis);

                if (this .rpcHook != null) {

                    this .rpcHook .doAfterResponse(request, response);

                }

                return response;

            }

            catch (RemotingSendRequestException e) {

                log .warn("invokeSync: send request exception, so close the channel[{}]", addr);

                this .closeChannel(addr, channel);

                throw e;

            }

            catch (RemotingTimeoutException e) {

                log .warn("invokeSync: wait response timeout exception, the channel[{}]", addr);

                // 超时异常如果关闭连接可能会产生连锁反应

                // this.closeChannel( addr, channel);

                throw e;

            }

        }

        else {

            this .closeChannel(addr, channel);

            throw new RemotingConnectException(addr);

        }

    }

 

这个方法大体分为两步,第一步获取连接,第二步通过连接发送请求,获取连接当然是getAndCreateChannel方法了,getAndCreateChannel方法非常重要,它包含了客户端对nameserver的failover,也包含了自动重连功能,对于客户端,传入的addr参数都是null,所以一直会走到getAndCreateNameserverChannel()方法。

   private Channel getAndCreateChannel( final String addr) throws InterruptedException {

        //无论是producer还是consumer,传进来的addr参数都是null

        if (null == addr)

            return getAndCreateNameserverChannel();

 

        //因为客户端传入的addr是null,所以客户端不会走到这里来,只有broker才会走到这里来,因为broker传入的addr不为null

        ChannelWrapper cw = this .channelTables .get(addr);

        if (cw != null && cw.isOK()) {

            return cw.getChannel();

        }

 

        //注意,如果和某个addr的连接不OK了,则再向该nameserver发起重连

        return this .createChannel(addr);

    }

 

createChannel方法很简单,无非就是创建连接嘛,就不细看了,分析下getAndCreateNameserverChannel(),以下是该方法大致过程:

因为客户端都是与某一台nameserver长连接,因此长连接一旦选定,后面不会变化,除非nameserver挂掉,所以已建立的长连接要保存起来。下面这段逻辑就是如此。

       String addr = this .namesrvAddrChoosed .get();

        if (addr != null) {

            ChannelWrapper cw = this .channelTables .get(addr);

             //注意这里,虽然长连接已经建立了,但是每次调用时,仍然要通过“cw != null && cw.isOK()”检查连接是否OK。

             if (cw != null && cw.isOK()) {

                return cw.getChannel();

            }

        }

 

如果连接没有建立或连接已经断开,则继续往下,真正创建连接时需要加锁的

 if ( this.lockNamesrvChannel .tryLock(LockTimeoutMillis, TimeUnit.MILLISECONDS ))

下面的代码都是在这个if块里面

这里又执行了一边上面的获取连接并检测的代码,可以连接,因为有时候连接只是偶尔不OK的

     addr = thisnamesrvAddrChoosed .get();

                if (addr != null) {

                    ChannelWrapper cw = this .channelTables .get(addr);

                    if (cw != null && cw.isOK()) {

                        return cw.getChannel();

                    }

                }

 

接着往下,这段代码非常重要

namesrvIndex指示了当前跟哪个nameserver发生连接,初始值是个随机数,跟nameserver数量取模,走到这一步,要么是首次发起调用,之前连接还未创建现在要创建了,或者是已创建的连接无效了要连接下一个nameserver,就是“cw.isOK()”为false。

 

 

        if (addrList != null && !addrList.isEmpty()) {

                    for (int i = 0; i < addrList.size(); i++) {

                        int index = this .namesrvIndex .incrementAndGet();

                        index = Math. abs(index);

                        index = index % addrList.size();

                        String newAddr = addrList.get(index);

 

                        this .namesrvAddrChoosed.set(newAddr);

                        Channel channelNew = this .createChannel(newAddr);

                        if (channelNew != null)

                            return channelNew;

                    }

                }

sharedCode源码交流群,欢迎喜欢阅读源码的朋友加群,添加下面的微信, 备注”加群“ 。 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值