Apache RocketMQ源码学习之生产者启动

源码地址:https://gitee.com/pengyd950812/rocket

 

一、RocketMQ核心组件以及关系

  • 核心组件:  producer:消息生产者,生产者的作用就是将消息发送到 MQ  
  • producer group:生产者组,简单来说就是多个发送同一类消息的生产者称之为一个生产者组
  • consumer :消息消费者,简单来说,消费 MQ 上的消息的应用程序就是消费者  
  • consumer group:消费者组,和生产者类似,消费同一类消息的多个 consumer 实例组成一个消费者组  
  • topic:是一种消息的逻辑分类
  • message : Message 是消息的载体。一个 Message 必须指定 topic,相当于寄信的地址。Message 还有一个可选的 tag 设置,以便消费端可以基于 tag 进行过滤消息
  • tag : 标签可以被认为是对 Topic 进一步细化。一般在相同业务模块中通过引入标签来标记不同用途的消息
  • broker : broker 接收来自生产者的消息,储存以及为消费者拉取消息的请求做好准备
  • Name Server : Name Server 为消息生产者和消费者提供路由信息。

 

二、核心组件之间的关系

① Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。

② Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Broker(Master)建立长连接,且定时向Broker(Master)发送心跳。

③ Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。

 

三、生产者启动过程(图)

Apache RocketMQ源码学习之生产者启动_第1张图片

 

四、跟着源码阅读其实现过程

1.DefaultMQProducerImpl类的start()方法  - (启动的全部流程)

public void start(final boolean startFactory) throws MQClientException {
        switch (this.serviceState) {
            // 如果当前服务状态为CREATE_JUST【刚创建】
            case CREATE_JUST:
                // 更改状态防止多次启动
                this.serviceState = ServiceState.START_FAILED;

                // groupName合法性校验
                this.checkConfig();

                // 判断当前生产者组是否符合要求
                // 改变生产者的实例id为进程id
                if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                    this.defaultMQProducer.changeInstanceNameToPID();
                }

                // MQClientInstance 负责与NameServer 通信进行心跳维持、根据Topic获取Broker地址;
                // MQClientInstance 负责与Broker通信进行收发消息、ReBalance;

                // 根据clientId获取对应的MQClientInstance,同一个clientId会复用同一个MQClientInstance
                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);

                // 将producer注册到MQClientInstance.producerTbale
                boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
                if (!registerOK) {
                    // 注册失败,状态==仅创建
                    this.serviceState = ServiceState.CREATE_JUST;
                    throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
                        + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                        null);
                }

                // 注册成功则将当前生产者组对应的topic与发布关系放入topicPublishInfoTable注册表
                // 缓存topic的路由信息
                this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

                if (startFactory) {
                    // 启动MQClientInstance
                    mQClientFactory.start();
                }

                log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
                    this.defaultMQProducer.isSendMessageWithVIPChannel());
                this.serviceState = ServiceState.RUNNING;
                break;
            case RUNNING:
            case START_FAILED:
            case SHUTDOWN_ALREADY:
                throw new MQClientException("The producer service state not OK, maybe started once, "
                    + this.serviceState
                    + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                    null);
            default:
                break;
        }
        // 生产者启动时与某一个nameServer建立长连接,并定时从该nameServer拉取topic对应路由信息
        // 生产者启动时与所有Broker建立长连接,并定时发送心跳请求,定时清除本地无效broker信息

        // 向所有 broker发送心跳
        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();

        this.timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                try {
                    RequestFutureTable.scanExpiredRequest();
                } catch (Throwable e) {
                    log.error("scan RequestFutureTable exception", e);
                }
            }
        }, 1000 * 3, 1000);
    }

2.MQClientInstance类是负责生产者与NameServer和Broker通信的核心类。看看它的实现

this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
   public MQClientInstance getOrCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
        // 这种情况会出现在你在一个JVM里启动了多个Producer时,
        // 且没有设置instanceName和unitName,那么这两个Producer会公用一个MQClientInstance,发送的消息会路由到同一个集群。
        // 例如,你起了两个Producer,并且配置的NameServer地址不一样,本意是让这两个Producer往不同集群上分配消息,
        // 但是由于共用了一个MQClientInstance,这个MQClientInstance是基于先来的Producer配置构建的,
        // 第二个Producer和他公用后被认为是同一instance,配置是相同的,消息的路由就是相同的,就没有达到你想要的效果。

        // 构建MQClientId
        String clientId = clientConfig.buildMQClientId();
        // 从clientId与MQClientInstance映射表factoryTable中获取当前clientId对应的MQClientInstance
        MQClientInstance instance = this.factoryTable.get(clientId);

        // 如果MQClientInstance不存在则创建一个新的并放入映射表factoryTable中
        if (null == instance) {
            instance =
                new MQClientInstance(clientConfig.cloneClientConfig(),
                    this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
            MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
            if (prev != null) {
                instance = prev;
                log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
            } else {
                log.info("Created new MQClientInstance for clientId:[{}]", clientId);
            }
        }

        return instance;
    }

3. // 将producer注册到MQClientInstance.producerTbale (这里用的是线程安全的ConcurrentMap,group为key,实例为value)

boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);


private final ConcurrentMap producerTable = new ConcurrentHashMap();



public boolean registerProducer(final String group, final DefaultMQProducerImpl producer) {
        if (null == group || null == producer) {
            return false;
        }

        MQProducerInner prev = this.producerTable.putIfAbsent(group, producer);
        if (prev != null) {
            log.warn("the producer group[{}] exist already.", group);
            return false;
        }

        return true;
    }

4.// 注册成功则将当前生产者组对应的topic与发布关系放入topicPublishInfoTable注册表

// 缓存topic的路由信息

this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

5.关键类启动     mQClientFactory.start();

   public void start() throws MQClientException {
        // 同步当前实例
        synchronized (this) {
            switch (this.serviceState) {
                case CREATE_JUST:
                    this.serviceState = ServiceState.START_FAILED;
                    // 如果没有指定,就会通过默认的接口去获取name server的地址
                    if (null == this.clientConfig.getNamesrvAddr()) {
                        this.mQClientAPIImpl.fetchNameServerAddr();
                    }
                    // 启动用于通讯的客户端,内部是用Netty实现的,启动client与name节点的通讯服务
                    this.mQClientAPIImpl.start();
                    // 启动所有调度服务
                    this.startScheduledTask();
                    // 拉消息服务
                    this.pullMessageService.start();
                    // 开启负载均衡消费队列服务
                    this.rebalanceService.start();
                    // 启动消费者
                    this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
                    log.info("the client factory [{}] start OK", this.clientId);
                    this.serviceState = ServiceState.RUNNING;
                    break;
                case START_FAILED:
                    throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
                default:
                    break;
            }
        }
    }

这个方法是同步的

第一步会通过接口去获取NameServer地址

第二步启动用于通讯的客户端,内部是用Netty实现的,启动client与name节点的通讯服务。

第三步启动所有调度服务。底层开启很多定时任务来实现的。(以下五个:

2分钟发送请求来获取NameServer地址来动态更新NameServer地址/
30s 定时的从 NameServer 中获取 Topic、broker、queue 相关信息并更新
30s 定时清理无效的Broker,并向所有的Broker 发送心跳数据
5s 持久化消费偏移量(持久化到远程或者本地)
1s 调整push方式下的拉取线程数

第四步启动拉消息服务(开启一个线程)

 public void run() {
        log.info(this.getServiceName() + " service started");

        while (!this.isStopped()) {
            try {
                //从LinkedBlockingQueue中拉取pullRequest
                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");
    }

第五步开启负载均衡消费队列服务(开启一个线程)

@Override
    public void run() {
        log.info(this.getServiceName() + " service started");

        while (!this.isStopped()) {
            // 每隔20秒做一次负载均衡
            this.waitForRunning(waitInterval);
            this.mqClientFactory.doRebalance();
        }

        log.info(this.getServiceName() + " service end");
    }

第六步启动消费者(这里再启动一次的疑问,等看了消费者启动过程就明白了,暂时先放着)

6.向所有 broker发送心跳

        // 生产者启动时与某一个nameServer建立长连接,并定时从该nameServer拉取topic对应路由信息
        // 生产者启动时与所有Broker建立长连接,并定时发送心跳请求,定时清除本地无效broker信息

        // 向所有 broker发送心跳
        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();

        this.timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                try {
                    RequestFutureTable.scanExpiredRequest();
                } catch (Throwable e) {
                    log.error("scan RequestFutureTable exception", e);
                }
            }
        }, 1000 * 3, 1000);

五、总结

1、生产者启动的时候会检查相关配置:生产组是否配置, 必须配置且不能为null也不能为DEFAULT_PRODUCER

2、生产者实例名的设置,若未主动设置,则采用默认的配置, 生成的实例名(格式):pid@hostname

3、获得生产者的真正意义上的实例,并且以组名为key值, 将其缓存在缓存中(此处,生产者与组应是一一对应)

4、topicPublishInfoTable会初始化一个默认的topic信息,TBW102

5、MQ客户端实例开启

6、向所有的broker发送心跳

你可能感兴趣的:(mq)