RocketMQ(二)--梳理消费端(DefaultMQPushConsumer)的脉络

概要

我们先大致梳理消费端的主要逻辑,不纠结细节。先对主脉络有了清晰认识后,再抓细节才事半功倍。

消费端代码

public static void main(String[] args) throws InterruptedException, MQClientException {

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("comsumer2");
        consumer.setNamesrvAddr("192.168.182.3:9876");
        consumer.subscribe("TopicTest", "*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("Consumer Started.%n");
    }

开始扒代码

本人看的代码是4.9.0版本。
主要的类继承结构图,其中DefaultMQPushConsumerImpl和DefaultMQPushConsumer是互相组合的关系。
在这里插入图片描述

构造器

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("comsumer2");

默认使用平均分配的客户端负载均衡策略,rpcHook钩子函数默认为null。

public DefaultMQPushConsumer(final String consumerGroup) {
        this(null, consumerGroup, null, new AllocateMessageQueueAveragely());
    }

在DefaultMQPushConsumer里初始化了DefaultMQPushConsumerImpl。

public DefaultMQPushConsumer(final String namespace, final String consumerGroup, RPCHook rpcHook,
        AllocateMessageQueueStrategy allocateMessageQueueStrategy) {
        this.consumerGroup = consumerGroup;
        this.namespace = namespace;
        this.allocateMessageQueueStrategy = allocateMessageQueueStrategy;
        defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook);
    }

子类很简单,简单初始化了几个参数。这样父子类都拥有彼此的对象属性信息了。

public DefaultMQPushConsumerImpl(DefaultMQPushConsumer defaultMQPushConsumer, RPCHook rpcHook) {
        this.defaultMQPushConsumer = defaultMQPushConsumer;
        this.rpcHook = rpcHook;
        // 1000ms 发生异常的时候,延迟多久再拉取一次
        this.pullTimeDelayMillsWhenException = defaultMQPushConsumer.getPullTimeDelayMillsWhenException();
    }

小结

构造器重载,使得客户端可以灵活初始化对象,没有指定的参数使用默认值。

设置名称服务地址

这个很简单,在ClientConfig设置了名称服务的地址而已。

consumer.setNamesrvAddr("192.168.182.3:9876");
public void setNamesrvAddr(String namesrvAddr) {
        this.namesrvAddr = namesrvAddr;
    }

订阅

客户端订阅主题,除了允许对tag进行筛选过滤外,还支持使用sql92语法,对消息进行过滤。

consumer.subscribe("TopicTest", "*");
public void subscribe(String topic, String subExpression) throws MQClientException {
        try {
            // 生成一个SubscriptionData订阅数据对象
            SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subExpression);
            // 将订阅信息放到缓存中
            this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
            if (this.mQClientFactory != null) {
            	// 如果不为空,发送一个心跳包给所有的broker。mQClientFactory在comsumer.start()的时候会创建,所以此时不会进入这里
                this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
            }
        } catch (Exception e) {
            throw new MQClientException("subscription exception", e);
        }
    }

小结

订阅方法只是生成一个新的订阅数据对象,并放在缓存中。

注册消费的事件监听器

将并发事件监听器设置到defaultMQPushConsumerImpl和defaultMQPushConsumer而已。

public void registerMessageListener(MessageListenerConcurrently messageListener) {
        this.messageListener = messageListener;
        this.defaultMQPushConsumerImpl.registerMessageListener(messageListener);
    }

启动消费者

public void start() throws MQClientException {
		// 简单将自定义的comsumerGroup转成统一命名,这里转化后的值为tranditionalCsmGroup
		// 这里namespace为null,consumerGroup为tranditionalCsmGroup
        setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup));
        this.defaultMQPushConsumerImpl.start();
        if (null != traceDispatcher) {
            try {
                traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
            } catch (MQClientException e) {
                log.warn("trace dispatcher start failed ", e);
            }
        }
    }

进入this.defaultMQPushConsumerImpl.start()方法,主要是初始化和设置一些值、启动定时任务和启动消费者监听端口等。

public synchronized void start() throws MQClientException {
        switch (this.serviceState) {
            // 开始走这个case
            case CREATE_JUST:
                log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
                    this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
                // 先设置状态为失败
                this.serviceState = ServiceState.START_FAILED;
				// 基本校验,如果不满足启动消费者的任必要一条件会抛异常。有一部分校验是有默认值的,这里为了拓展修改源码的时候有问题,所以也做了校验
                this.checkConfig();
				// 拷贝订阅信息到rebalanceImpl的subscriptionInner缓存中,启动的时候这里没订阅信息
                this.copySubscription();
				// 集群模式下,如果ClientConfig的instanceName没有配置,取默认值default,这随机生成一个instanceName给该客户端
                if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
                    this.defaultMQPushConsumer.changeInstanceNameToPID();
                }
				// 初始化MQClientInstance mQClientFactory客户端实例,拥有netty的能力,可以进行网络请求
                this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
				// 设置rebalanceImpl,重负载
                this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
                this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
                this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
                this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
				// 创建pullAPIWrapper
                this.pullAPIWrapper = new PullAPIWrapper(
                    mQClientFactory,
                    this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
                // 消息被客户端过滤时会回调hook
                this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
				// 启动的时候为null
                if (this.defaultMQPushConsumer.getOffsetStore() != null) {
                    this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
                } else {
                	// 根据广播模式/集群模式,设置offsetStore,用来缓存客户端消费情况,在哪个主题下的哪个brokerName下的哪个queueId,消费到队列的哪个下标。广播模式还会将消费进度落地到本地某个目录下,集群模式的消费进度仅仅缓存在本地内存中,在远程broker落地
                    switch (this.defaultMQPushConsumer.getMessageModel()) {
                        case BROADCASTING:
                            this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                            break;
                        case CLUSTERING:
                            this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                            break;
                        default:
                            break;
                    }
                    this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
                }
                // 如果是广播模式,会取本地将消费信息从文件load到内存
                this.offsetStore.load();
				// 根据顺序消费/并发消费 初始化consumeMessageService 和 consumeOrderly 
                if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
                    this.consumeOrderly = true;
                    this.consumeMessageService =
                        new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
                } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
                    this.consumeOrderly = false;
                    this.consumeMessageService =
                        new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
                }
				// 启动客户端消费者服务,并发服务会启动一个定时任务,每15minutes清除过期消息,并将清除的过期消息发送回broker。
                this.consumeMessageService.start();
				// 将consumerGroup - defaultMQPushConsumerImpl注册到到消费者列表consumerTable
                boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
                if (!registerOK) {
                	// 如果注册失败,回滚状态,关闭线程池并抛异常
                    this.serviceState = ServiceState.CREATE_JUST;
                    this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown());
                    throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
                        + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                        null);
                }
				// 启动MQClientInstance,会启动PullMessageService和RebalanceService
                mQClientFactory.start();
                log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
                // 客户端启动完成,修改客户端服务状态为RUNNING
                this.serviceState = ServiceState.RUNNING;
                break;
            case RUNNING:
            case START_FAILED:
            case SHUTDOWN_ALREADY:
                throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
                    + this.serviceState
                    + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                    null);
            default:
                break;
        }
		// 从NameServer更新topic路由和订阅信息
        this.updateTopicSubscribeInfoWhenSubscriptionChanged();
        this.mQClientFactory.checkClientInBroker();
        // 发送心跳包给所有broker
        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
        // 重新做一次客户端负载均衡
        this.mQClientFactory.rebalanceImmediately();
    }

主要的启动流程清晰了,现在看看mQClientFactory客户端实例的启动过程。

mQClientFactory.start();
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()) {
                        // 如果没有设置名称服务地址,会自动抓取。抓取url = http:// + rocketmq.namesrv.domain路径+/rocketmq/nsaddr
                        this.mQClientAPIImpl.fetchNameServerAddr();
                    }
                    // Start request-response channel
                    // 依赖remotingClient,这个对象依赖netty,是客户端发送请求的对象
                    this.mQClientAPIImpl.start();
                    // Start various schedule tasks
                    // 启动定时任务,每两分钟向远程服务器拉取名称服务地址信息
                    // 启动定时任务,每30秒更新主题路由信息
                    // 启动定时任务,每30秒清理下线的broker、发送一次客户端心跳
                    // 启动定时任务,每5秒持久化所有consumer的消费信息
                    // 启动定时任务,每1秒检查线程池适配
                    this.startScheduledTask();
                    // Start pull service
                    // 启动一个线程,从pullRequestQueue队列中阻塞获取消息,给到对应的consumer消费,如果消费失败了,3s后重新放回到pullRequestQueue队列中等待被再次消费
                    this.pullMessageService.start();
                    // Start rebalance service
                    // 定时重新分配consumer消费哪些消息队列的消息,默认20s,可配置rocketmq.client.rebalance.waitInterval
                    this.rebalanceService.start();
                    // Start push service
                    // 将producer注册到生产者列表
                    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;
            }
        }
    }

小结

  1. 在消费者启动流程里,RocketMQ会启动各种线程去完成各自的任务
  2. 底层使用netty和broker通信
  3. 使用volatile关键字修饰的变量完成线程间通信
  4. 底层使用阻塞队列获取消息

总结

还有一些细节问题等到把生产者和broker做的事情梳理清晰了再画个流程图帮助理解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值