重新认识RocketMQ(6) - 定时消息

187 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

简述

RocketMQ的定时消息是设置某个时间定时发送给消费者的,在需要定时任务和消息队列结合的场景下可以很好代替定时任务,例如定时开奖、限时结束的活动等场景使用,以下引用阿里云对RocketMQ的说明做下解释。并解析定时消息的源码,理解其中的实现原理。

概念

定时消息:Producer将消息发送到消息队列RocketMQ,但并不期望立马投递这条消息,而是推迟到在当前时间点之后的某一个时间投递到Consumer进行消费,该消息即定时消息。 延时消息:Producer将消息发送到消息队列RocketMQ,但并不期望立马投递这条消息,而是延迟一定时间后才投递到Consumer进行消费,该消息即延时消息。

使用场景

定时消息和延时消息适用于以下一些场景:

消息生产和消费有时间窗口要求,例如在电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条延时消息。这条消息将会在30分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。如支付未完成,则关闭订单。如已完成支付则忽略。 通过消息触发一些定时任务,例如在某一固定时间点向用户发送提醒消息。

注意事项

定时消息的精度会有1s~2s的延迟误差。 源码如下:

    public long computeDeliverTimestamp(final int delayLevel, final long storeTimestamp) {
        Long time = this.delayLevelTable.get(delayLevel);
        if (time != null) {
            return time + storeTimestamp;
        }

        return storeTimestamp + 1000;
    }
  • 定时和延时消息的msg.setStartDeliverTime参数需要设置成当前时间戳之后的某个时刻(单位毫秒)。如果被设置成当前时间戳之前的某个时刻,消息将立刻投递给消费者。
  • 由于客户端和服务端可能存在时间差,消息的实际投递时间与客户端设置的投递时间之间可能存在偏差。

定时消息源码解析

对照源码与流程图进行理解

流程图

定时消息.drawio.png

源码如下:

DeliverDelayedMessageTimerTask#executeOnTimeup

        public void executeOnTimeup() {
		    // (1)
            ConsumeQueue cq =
                ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
                    delayLevel2QueueId(delayLevel));

            if (cq == null) {
			    // (2)
                this.scheduleNextTimerTask(this.offset, DELAY_FOR_A_WHILE);
                return;
            }
            // (3)
            SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
            if (bufferCQ == null) {
                // ...省略

                this.scheduleNextTimerTask(resetOffset, DELAY_FOR_A_WHILE);
                return;
            }

            long nextOffset = this.offset;
            try {
                int i = 0;
                ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
				// (4)
                for (; i < bufferCQ.getSize() && isStarted(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
                    long offsetPy = bufferCQ.getByteBuffer().getLong();
                    int sizePy = bufferCQ.getByteBuffer().getInt();
                    long tagsCode = bufferCQ.getByteBuffer().getLong();

                    if (cq.isExtAddr(tagsCode)) {
                        if (cq.getExt(tagsCode, cqExtUnit)) {
                            tagsCode = cqExtUnit.getTagsCode();
                        } else {
                            //can't find ext content.So re compute tags code.
                            log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
                                tagsCode, offsetPy, sizePy);
                            long msgStoreTime = defaultMessageStore.getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
							// (5)
                            tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
                        }
                    }

                    long now = System.currentTimeMillis();
					// (6)
                    long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
                    nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);

                    long countdown = deliverTimestamp - now;
					// (7)
                    if (countdown > 0) {
                        this.scheduleNextTimerTask(nextOffset, DELAY_FOR_A_WHILE);
                        return;
                    }

					// (8)
                    MessageExt msgExt = ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(offsetPy, sizePy);
                    if (msgExt == null) {
                        continue;
                    }

					// (9)
                    MessageExtBrokerInner msgInner = ScheduleMessageService.this.messageTimeup(msgExt);
                    if (TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC.equals(msgInner.getTopic())) {
                        log.error("[BUG] the real topic of schedule msg is {}, discard the msg. msg={}",
                            msgInner.getTopic(), msgInner);
                        continue;
                    }

                    boolean deliverSuc;
					// (10)
                    if (ScheduleMessageService.this.enableAsyncDeliver) {
                        deliverSuc = this.asyncDeliver(msgInner, msgExt.getMsgId(), nextOffset, offsetPy, sizePy);
                    } else {
                        deliverSuc = this.syncDeliver(msgInner, msgExt.getMsgId(), nextOffset, offsetPy, sizePy);
                    }

                    // 省略...
                }

               // 省略...
            }

            // 省略...
        }


解析

  • (1):findConsumeQueue查找ConsumeQueue,有的话使用旧的,没有会创建新的返回
  • (2):scheduleNextTimerTask放到下次任务执行,后面有很多判断都可能进入到下次执行
  • (3):根据offset获取MappedBuffer
  • (4):以存储单位ConsumeQueue.CQ_STORE_UNIT_SIZE进行每条消息发送判断
  • (5):计算发送时间
  • (6):校正发送的时间,时间在当前之后,设置为now
  • (7):时间在当前之后,下次再发送(这个不太理解)
  • (8):从内存中取出消息内容
  • (9):拼装消息
  • (10):同步/异步 投递消息

小结

个人对于RocketMQ的理解通过看源码、查网上资料、画流程图这些方法,其中不免有很多地方理解错误,希望看到的小伙伴能一起交流,共同进步~