顺序消息

1.顺序消息

1.1消息队列负载

RocketMQ 首先需要通过 RebalanceService 线程实现消息队列的负载, 集群模式下同一个消 费组 内的消 费者共同承担其订阅主题下消息队列的消费, 同一个消息消费队列在同一时刻只会被消费组内一个消费者消费, 一个消费者同一时刻可以分配多个消费队列

List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
for (MessageQueue mq : mqSet) {
    if (!this.processQueueTable.containsKey(mq)) {
        if (isOrder && !this.lock(mq)) {
            log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
            continue;
        }

        this.removeDirtyOffset(mq);
        ProcessQueue pq = new ProcessQueue();
        long nextOffset = this.computePullFromWhere(mq);
        if (nextOffset >= 0) {
            ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
            if (pre != null) {
                log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
            } else {
                log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
                PullRequest pullRequest = new PullRequest();
                pullRequest.setConsumerGroup(consumerGroup);
                pullRequest.setNextOffset(nextOffset);
                pullRequest.setMessageQueue(mq);
                pullRequest.setProcessQueue(pq);
                pullRequestList.add(pullRequest);
                changed = true;
            }
        } else {
            log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
        }
    }
}

如果经过消息队列重新负载(分配)后 ,分配到新的消息 队列时 ,首先需要尝试Broker 发起锁定该消息队列的请求,如果返回加锁成功则创建该消息队列的拉取任务,否则将跳过 ,等待其他消费者释放该消息 队列的锁 ,然后在下一次队列重新负载时再尝试加锁

1.2消息拉取

RocketMQ 消息拉取由 PullMessageService 线程负责,根据消息拉取任务循环拉取消息

if (processQueue.isLocked()) {
                if (!pullRequest.isLockedFirst()) {
                    final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
                    boolean brokerBusy = offset < pullRequest.getNextOffset();
                    log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
                        pullRequest, offset, brokerBusy);
                    if (brokerBusy) {
                        log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
                            pullRequest, offset);
                    }

                    pullRequest.setLockedFirst(true);
                    pullRequest.setNextOffset(offset);
                }
            } else {
                this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
                log.info("pull message later because not locked in broker, {}", pullRequest);
                return;
            }

如果消息处理队列未被锁定,则延迟 3s 后再将 PullRequest 对象放入到拉取任务中,

如果该处 队列是第一次拉取 ,则 先计算拉取偏移量,然后 向消息服务端拉取消息

1.3消息消费

顺序消息消费的实现类 org.apache.rocketmq.client.impl consumer.ConsumeMessageOrderlyService

this.consumeExecutor = new ThreadPoolExecutor(
    this.defaultMQPushConsumer.getConsumeThreadMin(),
    this.defaultMQPushConsumer.getConsumeThreadMax(),
    1000 * 60,
    TimeUnit.MILLISECONDS,
    this.consumeRequestQueue,
    new ThreadFactoryImpl("ConsumeMessageThread_"));

初始化实例参数,这里的关键是消息任务队列为 LinkedB!ockingQueue ,消息消费线程最大运行时线程个数为 onsumeThreadMin consumeThreadMa 参数将失效

public void start() {
    if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                ConsumeMessageOrderlyService.this.lockMQPeriodically();
            }
        }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);
    }
}

如果消费模式为集群模式,启动定时任务,默认每隔 20s 执行 一次锁定分配给自 己的消息消费队列 通过 Drocketmq. client.rebalance. locklnterval =20000 设置间隔,该值建议与一次消息负载频率设置相同, 从上文可知,集群模式下顺序消息消费在创建拉取任务时并

未将 ProcessQu ue locked 态设置为 true 在未锁定消息队列之前无法执行消息拉取任务, ConsumeM ssageOrderlyService 0s 频率对分配给自己的消息队列进行自动锁操作,从而消费加锁成功的消息消费队列

private void submitConsumeRequestLater(
    final ProcessQueue processQueue,
    final MessageQueue messageQueue,
    final long suspendTimeMillis
) {
    long timeMillis = suspendTimeMillis;
    if (timeMillis == -1) {
        timeMillis = this.defaultMQPushConsumer.getSuspendCurrentQueueTimeMillis();
    }

    if (timeMillis < 10) {
        timeMillis = 10;
    } else if (timeMillis > 30000) {
        timeMillis = 30000;
    }

    this.scheduledExecutorService.schedule(new Runnable() {

        @Override
        public void run() {
            ConsumeMessageOrderlyService.this.submitConsumeRequest(null, processQueue, messageQueue, true);
        }
    }, timeMillis, TimeUnit.MILLISECONDS);
}
@Override
public void submitConsumeRequest(
    final List<MessageExt> msgs,
    final ProcessQueue processQueue,
    final MessageQueue messageQueue,
    final boolean dispathToConsume) {
    if (dispathToConsume) {
        ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
        this.consumeExecutor.submit(consumeRequest);
    }
}

构建消费任务 ConsumeRequest 并提交到消费线程池

if (this.processQueue.isDropped()) {
    log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
    return;
}

如果消息处理队列为丢弃, 停止本次消费任务

final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {

根据消息队列获取 一个对象 然后消息消费时先申请独占 objLock,顺序消息消费的并发度为消息队列 ,也就是一个消息消费队列同 时刻只会被一个消 费线程池中一个线程消费

if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
    || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {

        if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
            && !this.processQueue.isLocked()) {
            log.warn("the message queue not locked, so consume later, {}", this.messageQueue);
            ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
            break;
        }

     
} else {
    if (this.processQueue.isDropped()) {
        log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
        return;
    }

    ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
}

如果是广播模式的话 ,直接进入消费,无须锁定处理队列,因为相互直接无竞争; 如果是集群模式,消息消费的前提条件 proceessQueue被锁定并且锁未超时 ,思考下,会不会出现当消息队列重新负载时,原先由自己处理的消息队列被另外一个消费者分配,此时如果还未来得及将 ProceeQueue 解除锁定,就被另外 一个消费者添加进去, 此时会存储多个消 息消费者同时消费 个消息队列?答案是不会 ,因为当 一个新的消费队列分配给消费者时, 在添加其拉取任务之前必须先向 Broker 发送对该消息队列加锁请求,只加锁成功后,才能添加拉取消息,否则到下一次负载后,只有消费队列被原先占有的消费者释放后,才能开始新的拉取任务 集群模式下,如果未锁定处理队列,则延迟该队列的消息消费

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值