欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

RocketMQ——RocketMQ消息存储

程序员文章站 2022-07-14 23:42:17
...

DefaultMQPushConsumer

属性
consumerGroup 消费组名称
messageModel 消息消费模式,分为集群模式和广播模式
consumeFromWhere 消费者开始消费的位置,默认为最大偏移量 CONSUME_FROM_LAST_OFFSET
allocateMessageQueueStrategy 集群模式下消费队列负载均衡策略
subscription 订阅信息
messageListener 消息业务监听器
offsetStore 消费进度存储器
consumeThreadMin 消费者最小线程数
consumeThreadMax 消费最大线程数
pullBatchSize 每次拉取消息size
DefaultMQPushConsumerImpl 核心实现,核心的方法都在这里实现

消费者启动流程

DefaultMQPushConsumer#start

 this.defaultMQPushConsumerImpl.start();

DefaultMQPushConsumerImpl#start

创建MQClient

this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);

负载均衡初始化

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());
                this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);

初始化 offsetStore 集群模式文件是存储在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;
                    }

consumeMessageService.start()

    public void start() {
        this.cleanExpireMsgExecutors.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                cleanExpireMsg();
            }

        }, this.defaultMQPushConsumer.getConsumeTimeout(), this.defaultMQPushConsumer.getConsumeTimeout(), TimeUnit.MINUTES);
    }

注册consumer

 boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);

MQClient启动

 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()) {
                        this.mQClientAPIImpl.fetchNameServerAddr();
                    }
                    // Start request-response channel
                    this.mQClientAPIImpl.start();
                    // Start various schedule tasks
                    this.startScheduledTask();
                    // Start pull service
                    this.pullMessageService.start();
                    // Start rebalance service
                    this.rebalanceService.start();
                    // Start push service
                    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.updateTopicSubscribeInfoWhenSubscriptionChanged();
        this.mQClientFactory.checkClientInBroker();
        this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
        this.mQClientFactory.rebalanceImmediately();

消息拉取

看到再MQ consumer启动过程中,会启动 mQClientFactory.start() 方法中,会启动对应的this.pullMessageService.start()
我们查看对应的PullMessageService。

PullMessageService

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

        while (!this.isStopped()) {
            try {
                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");
    }

PullRequest 的添加如下:
PullMessageService#executePullRequestLater

    public void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) {
        if (!isStopped()) {
            this.scheduledExecutorService.schedule(new Runnable() {
                @Override
                public void run() {
                    PullMessageService.this.executePullRequestImmediately(pullRequest);
                }
            }, timeDelay, TimeUnit.MILLISECONDS);
        } else {
            log.warn("PullMessageServiceScheduledThread has shutdown");
        }
    }
    public void executePullRequestImmediately(final PullRequest pullRequest) {
        try {
            this.pullRequestQueue.put(pullRequest);
        } catch (InterruptedException e) {
            log.error("executePullRequestImmediately pullRequestQueue.put", e);
        }
    }

DefaultMQPushConsumerImpl#pullMessage 则调用对应的方法。

PullRequest 简介

PullRequest结构如下

属性
private String consumerGroup 消费者组
private MessageQueue messageQueue 待拉取消费队列
private ProcessQueue processQueue 消息处理队列,从broker拉取的消息先存储到ProcessQueue,然后再提交到消费者线程池消费
private long nextOffset 待拉取的MessageQueue偏移量
private boolean lockedFirst = false 是否被锁定

MessageQueue结构如下:

属性
private String topic topic
private String brokerName brokername
private int queueId queueId

PullMessageService#pullMessage

    private void pullMessage(final PullRequest pullRequest) {
        final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
        if (consumer != null) {
            DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
            impl.pullMessage(pullRequest);
        } else {
            log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
        }
    }

ProcessQueue

ProcessQueue 用来存储broker拉取的消息,是MessageQueue在消费端的重现和快照。 PullMessageService每次默认拉取32条消息,按消息的队列偏移量顺序存在ProcessQueue中,PullMessageService将消息提交到消费者线程池,消费成功后从ProcessQueue中移除。
属性简介

ReadWriteLock lockTreeMap = new ReentrantReadWriteLock()
TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>() 消息存储器,key为消息再ConsumeQueue中的偏移量,MessageExt为消息体
AtomicLong msgCount = new AtomicLong() ProcessQueue中的消息总量
AtomicLong msgSize = new AtomicLong()
Lock lockConsume = new ReentrantLock()
TreeMap<Long, MessageExt> consumingMsgOrderlyTreeMap 顺序消息消费
AtomicLong tryUnlockTimes = new AtomicLong(0)
volatile long queueOffsetMax = 0L 当前ProcessQueue中包含的最大队列偏移量
volatile boolean dropped = false 当前ProcessQueue是否被丢弃
volatile long lastPullTimestamp = System.currentTimeMillis() 上次拉取消息时间戳
volatile long lastConsumeTimestamp = System.currentTimeMillis() 上次消息消费时间戳
volatile boolean locked = false
volatile long lastLockTimestamp = System.currentTimeMillis() 上次锁定时间戳
volatile boolean consuming = false
volatile long msgAccCnt = 0

消息拉取流程

消息拉取分为三个流程:

  1. 消息拉取客户端消息拉取请求封装
  2. 消费服务器查找并返回消息
  3. 消息拉取客户端处理返回的消息

如下图为consumer启动然后拉取消息的流程


RocketMQ——RocketMQ消息存储
D479A36DA39B4460DEC046151C77B517.jpg

最终PullMessageService 调用DefaultMQPushConsumerImpl中的pullMessage
DefaultMQPushConsumerImpl#pullMessage 核心调用方法:

 try {
            this.pullAPIWrapper.pullKernelImpl(
                pullRequest.getMessageQueue(),//拉取的消息队列
                subExpression,//消息过滤表达式
                subscriptionData.getExpressionType(),//消息表达式类型,TAG、SQL92
                subscriptionData.getSubVersion(),
                pullRequest.getNextOffset(),//消息拉取偏移量
                this.defaultMQPushConsumer.getPullBatchSize(),//本次拉取最大消息条数,默认32条
                sysFlag,//拉取系统标记
                commitOffsetValue,//当前MessageQueue的消费进度
                BROKER_SUSPEND_MAX_TIME_MILLIS,//消息拉取过程中允许broker挂起时间
                CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,//消息拉取超时时间
                CommunicationMode.ASYNC,//消息拉取模式,默认为异步拉取
                pullCallback//从broker拉取到消息后的回调方法
            );
        } catch (Exception e) {
            log.error("pullKernelImpl exception", e);
            this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
        }

PullAPIWrapper#pullKernelImpl

  1. 获取broker信息
  2. 封装request
  3. 发起调用,执行回调函数

MQClientAPIImpl#pullMessageSync

 this.remotingClient.invokeSync(addr, request, timeoutMillis)

Broker组装消息

RequestCode#PULL_MESSAGE 定位到Broker端处理消息拉取的入口 PullMessageProcessor#processRequest
核心代码只有一行

 final GetMessageResult getMessageResult =
            this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), requestHeader.getTopic(),
                requestHeader.getQueueId(), requestHeader.getQueueOffset(), requestHeader.getMaxMsgNums(), messageFilter);

MQClientAPIImpl#processPullResponse

在 PullAPIWrapper#pullKernelImpl 方法执行完毕后,执行 processPullResponse 来处理返回response

pullCallback.onSuccess(pullResult);

执行pullCallback回调函数

消息长轮询模式

RocketMQ并没有真正实现Push模式,而是循环向服务端发送拉取消息请求,拉取消息。

消息队列负载和重新分布机制

RocketMQ的负载均衡通过RebalanceService实现,每个MQClientInstance都持有RebalanceService实例。
RebalanceService#run


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

        while (!this.isStopped()) {
            this.waitForRunning(waitInterval);
            this.mqClientFactory.doRebalance();
        }

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

RebalanceImpl#doRebalance

    public void doRebalance(final boolean isOrder) {
        Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
        if (subTable != null) {
            for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
                final String topic = entry.getKey();
                try {
                    this.rebalanceByTopic(topic, isOrder);
                } catch (Throwable e) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                        log.warn("rebalanceByTopic Exception", e);
                    }
                }
            }
        }

        this.truncateMessageQueueNotMyTopic();
    }