RocketMQ——RocketMQ消息存储

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启动然后拉取消息的流程


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();
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容