RocketMQ(4.8.0)——延迟消息机制

延迟消息机制

一、延迟消息概述

  什么是延迟消息呢?延迟消息也叫定时消息,一般地,生产者在发送消息后,消费者希望在指定的一段时间后再消费。常规做法是,把信息存储在数据库中,使用定时任务扫描,符合条件的数据再发送给消费者。

  RocketMQ 延迟消息是通过 D:\rocketmq-master\store\src\main\java\org\apache\rocketmq\store\schedule\ScheduleMessageService.java 类来实现的。该类的核心属性和方法如下:

  1 /*
  2  * Licensed to the Apache Software Foundation (ASF) under one or more
  3  * contributor license agreements.  See the NOTICE file distributed with
  4  * this work for additional information regarding copyright ownership.
  5  * The ASF licenses this file to You under the Apache License, Version 2.0
  6  * (the "License"); you may not use this file except in compliance with
  7  * the License.  You may obtain a copy of the License at
  8  *
  9  *     http://www.apache.org/licenses/LICENSE-2.0
 10  *
 11  * Unless required by applicable law or agreed to in writing, software
 12  * distributed under the License is distributed on an "AS IS" BASIS,
 13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  * See the License for the specific language governing permissions and
 15  * limitations under the License.
 16  */
 17 package org.apache.rocketmq.store.schedule;
 18 
 19 import java.util.HashMap;
 20 import java.util.Iterator;
 21 import java.util.Map;
 22 import java.util.Timer;
 23 import java.util.TimerTask;
 24 import java.util.concurrent.ConcurrentHashMap;
 25 import java.util.concurrent.ConcurrentMap;
 26 import java.util.concurrent.atomic.AtomicBoolean;
 27 import org.apache.rocketmq.common.ConfigManager;
 28 import org.apache.rocketmq.common.TopicFilterType;
 29 import org.apache.rocketmq.common.constant.LoggerName;
 30 import org.apache.rocketmq.common.topic.TopicValidator;
 31 import org.apache.rocketmq.logging.InternalLogger;
 32 import org.apache.rocketmq.logging.InternalLoggerFactory;
 33 import org.apache.rocketmq.common.message.MessageAccessor;
 34 import org.apache.rocketmq.common.message.MessageConst;
 35 import org.apache.rocketmq.common.message.MessageDecoder;
 36 import org.apache.rocketmq.common.message.MessageExt;
 37 import org.apache.rocketmq.common.running.RunningStats;
 38 import org.apache.rocketmq.store.ConsumeQueue;
 39 import org.apache.rocketmq.store.ConsumeQueueExt;
 40 import org.apache.rocketmq.store.DefaultMessageStore;
 41 import org.apache.rocketmq.store.MessageExtBrokerInner;
 42 import org.apache.rocketmq.store.MessageStore;
 43 import org.apache.rocketmq.store.PutMessageResult;
 44 import org.apache.rocketmq.store.PutMessageStatus;
 45 import org.apache.rocketmq.store.SelectMappedBufferResult;
 46 import org.apache.rocketmq.store.config.StorePathConfigHelper;
 47 
 48 public class ScheduleMessageService extends ConfigManager {
 49     private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
 50 
 51     private static final long FIRST_DELAY_TIME = 1000L;
 52     private static final long DELAY_FOR_A_WHILE = 100L;
 53     private static final long DELAY_FOR_A_PERIOD = 10000L;
 54 
 55     private final ConcurrentMap<Integer /* level */, Long/* delay timeMillis */> delayLevelTable =
 56         new ConcurrentHashMap<Integer, Long>(32);
 57 
 58     private final ConcurrentMap<Integer /* level */, Long/* offset */> offsetTable =
 59         new ConcurrentHashMap<Integer, Long>(32);
 60     private final DefaultMessageStore defaultMessageStore;
 61     private final AtomicBoolean started = new AtomicBoolean(false);
 62     private Timer timer;
 63     private MessageStore writeMessageStore;
 64     private int maxDelayLevel;
 65 
 66     public ScheduleMessageService(final DefaultMessageStore defaultMessageStore) {
 67         this.defaultMessageStore = defaultMessageStore;
 68         this.writeMessageStore = defaultMessageStore;
 69     }
 70 
 71     public static int queueId2DelayLevel(final int queueId) {
 72         return queueId + 1;
 73     }
 74 
 75     public static int delayLevel2QueueId(final int delayLevel) {
 76         return delayLevel - 1;
 77     }
 78 
 79     /**
 80      * @param writeMessageStore
 81      *     the writeMessageStore to set
 82      */
 83     public void setWriteMessageStore(MessageStore writeMessageStore) {
 84         this.writeMessageStore = writeMessageStore;
 85     }
 86 
 87     public void buildRunningStats(HashMap<String, String> stats) {
 88         Iterator<Map.Entry<Integer, Long>> it = this.offsetTable.entrySet().iterator();
 89         while (it.hasNext()) {
 90             Map.Entry<Integer, Long> next = it.next();
 91             int queueId = delayLevel2QueueId(next.getKey());
 92             long delayOffset = next.getValue();
 93             long maxOffset = this.defaultMessageStore.getMaxOffsetInQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC, queueId);
 94             String value = String.format("%d,%d", delayOffset, maxOffset);
 95             String key = String.format("%s_%d", RunningStats.scheduleMessageOffset.name(), next.getKey());
 96             stats.put(key, value);
 97         }
 98     }
 99 
100     private void updateOffset(int delayLevel, long offset) {
101         this.offsetTable.put(delayLevel, offset);
102     }
103 
104     public long computeDeliverTimestamp(final int delayLevel, final long storeTimestamp) {
105         Long time = this.delayLevelTable.get(delayLevel);
106         if (time != null) {
107             return time + storeTimestamp;
108         }
109 
110         return storeTimestamp + 1000;
111     }
112 
113     public void start() {
114         if (started.compareAndSet(false, true)) {
115             this.timer = new Timer("ScheduleMessageTimerThread", true);
116             for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
117                 Integer level = entry.getKey();
118                 Long timeDelay = entry.getValue();
119                 Long offset = this.offsetTable.get(level);
120                 if (null == offset) {
121                     offset = 0L;
122                 }
123 
124                 if (timeDelay != null) {
125                     this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
126                 }
127             }
128 
129             this.timer.scheduleAtFixedRate(new TimerTask() {
130 
131                 @Override
132                 public void run() {
133                     try {
134                         if (started.get()) ScheduleMessageService.this.persist();
135                     } catch (Throwable e) {
136                         log.error("scheduleAtFixedRate flush exception", e);
137                     }
138                 }
139             }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
140         }
141     }
142 
143     public void shutdown() {
144         if (this.started.compareAndSet(true, false)) {
145             if (null != this.timer)
146                 this.timer.cancel();
147         }
148 
149     }
150 
151     public boolean isStarted() {
152         return started.get();
153     }
154 
155     public int getMaxDelayLevel() {
156         return maxDelayLevel;
157     }
158 
159     public String encode() {
160         return this.encode(false);
161     }
162 
163     public boolean load() {
164         boolean result = super.load();
165         result = result && this.parseDelayLevel();
166         return result;
167     }
168 
169     @Override
170     public String configFilePath() {
171         return StorePathConfigHelper.getDelayOffsetStorePath(this.defaultMessageStore.getMessageStoreConfig()
172             .getStorePathRootDir());
173     }
174 
175     @Override
176     public void decode(String jsonString) {
177         if (jsonString != null) {
178             DelayOffsetSerializeWrapper delayOffsetSerializeWrapper =
179                 DelayOffsetSerializeWrapper.fromJson(jsonString, DelayOffsetSerializeWrapper.class);
180             if (delayOffsetSerializeWrapper != null) {
181                 this.offsetTable.putAll(delayOffsetSerializeWrapper.getOffsetTable());
182             }
183         }
184     }
185 
186     public String encode(final boolean prettyFormat) {
187         DelayOffsetSerializeWrapper delayOffsetSerializeWrapper = new DelayOffsetSerializeWrapper();
188         delayOffsetSerializeWrapper.setOffsetTable(this.offsetTable);
189         return delayOffsetSerializeWrapper.toJson(prettyFormat);
190     }
191 
192     public boolean parseDelayLevel() {
193         HashMap<String, Long> timeUnitTable = new HashMap<String, Long>();
194         timeUnitTable.put("s", 1000L);
195         timeUnitTable.put("m", 1000L * 60);
196         timeUnitTable.put("h", 1000L * 60 * 60);
197         timeUnitTable.put("d", 1000L * 60 * 60 * 24);
198 
199         String levelString = this.defaultMessageStore.getMessageStoreConfig().getMessageDelayLevel();
200         try {
201             String[] levelArray = levelString.split(" ");
202             for (int i = 0; i < levelArray.length; i++) {
203                 String value = levelArray[i];
204                 String ch = value.substring(value.length() - 1);
205                 Long tu = timeUnitTable.get(ch);
206 
207                 int level = i + 1;
208                 if (level > this.maxDelayLevel) {
209                     this.maxDelayLevel = level;
210                 }
211                 long num = Long.parseLong(value.substring(0, value.length() - 1));
212                 long delayTimeMillis = tu * num;
213                 this.delayLevelTable.put(level, delayTimeMillis);
214             }
215         } catch (Exception e) {
216             log.error("parseDelayLevel exception", e);
217             log.info("levelString String = {}", levelString);
218             return false;
219         }
220 
221         return true;
222     }
223 
224     class DeliverDelayedMessageTimerTask extends TimerTask {
225         private final int delayLevel;
226         private final long offset;
227 
228         public DeliverDelayedMessageTimerTask(int delayLevel, long offset) {
229             this.delayLevel = delayLevel;
230             this.offset = offset;
231         }
232 
233         @Override
234         public void run() {
235             try {
236                 if (isStarted()) {
237                     this.executeOnTimeup();
238                 }
239             } catch (Exception e) {
240                 // XXX: warn and notify me
241                 log.error("ScheduleMessageService, executeOnTimeup exception", e);
242                 ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
243                     this.delayLevel, this.offset), DELAY_FOR_A_PERIOD);
244             }
245         }
246 
247         /**
248          * @return
249          */
250         private long correctDeliverTimestamp(final long now, final long deliverTimestamp) {
251 
252             long result = deliverTimestamp;
253 
254             long maxTimestamp = now + ScheduleMessageService.this.delayLevelTable.get(this.delayLevel);
255             if (deliverTimestamp > maxTimestamp) {
256                 result = now;
257             }
258 
259             return result;
260         }
261 
262         public void executeOnTimeup() {
263             ConsumeQueue cq =
264                 ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
265                     delayLevel2QueueId(delayLevel));
266 
267             long failScheduleOffset = offset;
268 
269             if (cq != null) {
270                 SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
271                 if (bufferCQ != null) {
272                     try {
273                         long nextOffset = offset;
274                         int i = 0;
275                         ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
276                         for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
277                             long offsetPy = bufferCQ.getByteBuffer().getLong();
278                             int sizePy = bufferCQ.getByteBuffer().getInt();
279                             long tagsCode = bufferCQ.getByteBuffer().getLong();
280 
281                             if (cq.isExtAddr(tagsCode)) {
282                                 if (cq.getExt(tagsCode, cqExtUnit)) {
283                                     tagsCode = cqExtUnit.getTagsCode();
284                                 } else {
285                                     //can't find ext content.So re compute tags code.
286                                     log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
287                                         tagsCode, offsetPy, sizePy);
288                                     long msgStoreTime = defaultMessageStore.getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
289                                     tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
290                                 }
291                             }
292 
293                             long now = System.currentTimeMillis();
294                             long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
295 
296                             nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
297 
298                             long countdown = deliverTimestamp - now;
299 
300                             if (countdown <= 0) {
301                                 MessageExt msgExt =
302                                     ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
303                                         offsetPy, sizePy);
304 
305                                 if (msgExt != null) {
306                                     try {
307                                         MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
308                                         if (TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC.equals(msgInner.getTopic())) {
309                                             log.error("[BUG] the real topic of schedule msg is {}, discard the msg. msg={}",
310                                                     msgInner.getTopic(), msgInner);
311                                             continue;
312                                         }
313                                         PutMessageResult putMessageResult =
314                                             ScheduleMessageService.this.writeMessageStore
315                                                 .putMessage(msgInner);
316 
317                                         if (putMessageResult != null
318                                             && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
319                                             continue;
320                                         } else {
321                                             // XXX: warn and notify me
322                                             log.error(
323                                                 "ScheduleMessageService, a message time up, but reput it failed, topic: {} msgId {}",
324                                                 msgExt.getTopic(), msgExt.getMsgId());
325                                             ScheduleMessageService.this.timer.schedule(
326                                                 new DeliverDelayedMessageTimerTask(this.delayLevel,
327                                                     nextOffset), DELAY_FOR_A_PERIOD);
328                                             ScheduleMessageService.this.updateOffset(this.delayLevel,
329                                                 nextOffset);
330                                             return;
331                                         }
332                                     } catch (Exception e) {
333                                         /*
334                                          * XXX: warn and notify me
335 
336 
337 
338                                          */
339                                         log.error(
340                                             "ScheduleMessageService, messageTimeup execute error, drop it. msgExt="
341                                                 + msgExt + ", nextOffset=" + nextOffset + ",offsetPy="
342                                                 + offsetPy + ",sizePy=" + sizePy, e);
343                                     }
344                                 }
345                             } else {
346                                 ScheduleMessageService.this.timer.schedule(
347                                     new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
348                                     countdown);
349                                 ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
350                                 return;
351                             }
352                         } // end of for
353 
354                         nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
355                         ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
356                             this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);
357                         ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
358                         return;
359                     } finally {
360 
361                         bufferCQ.release();
362                     }
363                 } // end of if (bufferCQ != null)
364                 else {
365 
366                     long cqMinOffset = cq.getMinOffsetInQueue();
367                     if (offset < cqMinOffset) {
368                         failScheduleOffset = cqMinOffset;
369                         log.error("schedule CQ offset invalid. offset=" + offset + ", cqMinOffset="
370                             + cqMinOffset + ", queueId=" + cq.getQueueId());
371                     }
372                 }
373             } // end of if (cq != null)
374 
375             ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
376                 failScheduleOffset), DELAY_FOR_A_WHILE);
377         }
378 
379         private MessageExtBrokerInner messageTimeup(MessageExt msgExt) {
380             MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
381             msgInner.setBody(msgExt.getBody());
382             msgInner.setFlag(msgExt.getFlag());
383             MessageAccessor.setProperties(msgInner, msgExt.getProperties());
384 
385             TopicFilterType topicFilterType = MessageExt.parseTopicFilterType(msgInner.getSysFlag());
386             long tagsCodeValue =
387                 MessageExtBrokerInner.tagsString2tagsCode(topicFilterType, msgInner.getTags());
388             msgInner.setTagsCode(tagsCodeValue);
389             msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgExt.getProperties()));
390 
391             msgInner.setSysFlag(msgExt.getSysFlag());
392             msgInner.setBornTimestamp(msgExt.getBornTimestamp());
393             msgInner.setBornHost(msgExt.getBornHost());
394             msgInner.setStoreHost(msgExt.getStoreHost());
395             msgInner.setReconsumeTimes(msgExt.getReconsumeTimes());
396 
397             msgInner.setWaitStoreMsgOK(false);
398             MessageAccessor.clearProperty(msgInner, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
399 
400             msgInner.setTopic(msgInner.getProperty(MessageConst.PROPERTY_REAL_TOPIC));
401 
402             String queueIdStr = msgInner.getProperty(MessageConst.PROPERTY_REAL_QUEUE_ID);
403             int queueId = Integer.parseInt(queueIdStr);
404             msgInner.setQueueId(queueId);
405 
406             return msgInner;
407         }
408     }
409 }

  SCHEDULE_TOPIC:一个系统内置的  Topic,用来保存所有定时消息。RocketMQ 全部未执行的延迟消息保存在这个内部 Topic 中。

  First_DELAY_TIME:第一次执行定时任务的延迟时间,默认为 1000ms。

  DELAY_FOR_A_WHILE:第二次及以后的定时任务检查间隔时间,默认为 100ms。

  DELAY_FOR_A_PERIOD:如果延迟消息到时间投递时却失败了,会在 DELAY_FOR_A_PERIOD ms 后重新尝试投递,默认为 10 000ms。

  delayLevelTable:保存延迟级别和延迟时间的映射关系。

  offsetTable:保存延迟级别及相应的消费位点。

  timer:用于执行定时任务,线程名叫 ScheduleMessageTimerThread。

org.apache.rocketmq.store.schedule.ScheduleMessageService 类有如下两个核心方法。

  queueId2DelayLevel():将 quque id 转化为延迟级别。

  delayLevel2QueueId():将延迟级别转化为 queue id。

  在延迟消息 Topic 中,不同延迟级别的消息保存在不同的 Queue 中,代码路径:D:\rocketmq-master\store\src\main\java\org\apache\rocketmq\store\schedule\ScheduleMessageService.java

1     public static int queueId2DelayLevel(final int queueId) {
2         return queueId + 1;
3     }

  代码路径:D:\rocketmq-master\store\src\main\java\org\apache\rocketmq\store\schedule\ScheduleMessageService.java

1     public static int delayLevel2QueueId(final int delayLevel) {
2         return delayLevel - 1;
3     }

  从上面可以看到,一个延迟级别保存在一个 Queue 中,延迟级别和 Queue 之间的转化关系为 queueId=delayLevel-1。

ScheduleMessageService 类中的核心方法:

  updateOffset():更新延迟消息的 Topic 的消费位点。

  ComputeDeliverTimestamp():根据延迟级别和消息的存储时间计算延迟消息的投递时间。

  start():启动延迟消息服务。启动第一次延迟消息投递的检查定时任务和持久化消费位点的定时任务。

  shutdown():关闭 start() 方法中启动的 timer 任务。

  load():加载延迟消息的消费位点信息和全部延迟级别信息,延迟级别可以通过 messageDelayLevel 字段进行设置,默认值为 1s、5s、10s、30s、1m、2m、3m、4m、5m、6m、7m、8m、9m、10m、20m、30m、1h、2h。

  parseDelayLevel():格式化所有延迟级别信息,并保存到内存中。

  DeliverDelayedMessageTimerTask 内部类用于检查延迟消息是否可以投递,DeliverDelayedMessageTimerTask 是 TimerTask 的一个扩展实现。

二、延迟消息机制

  2.1 延迟消息存储机制

  在延迟消息的发送流程中,消息体会设置一个 delayTimeLevel,其他发送流程也是如此。Broker 在接收延迟消息时会有几个地方单独处理再存储,其余过程和普通消息存储一直。

  调用 CommitLog.putMessage()方法存储延迟消息,代码路径:D:\rocketmq-master\store\src\main\java\org\apache\rocketmq\store\CommitLog.java,具体代码如下:

 1             if (msg.getDelayTimeLevel() > 0) {
 2                 if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
 3                     msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
 4                 }
 5 
 6                 topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
 7                 queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
 8 
 9                 // Backup real topic, queueId
10                 MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
11                 MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
12                 msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
13 
14                 msg.setTopic(topic);
15                 msg.setQueueId(queueId);
16             }

  msg.setDelayTimeLevel() 是发送消息时可以设置的延迟级别,如果该值大于0,则表示当前处理的消息是一个延迟消息,将对该消息做如下修改:

    • 将原始 Topic、ququeId 备份在消息的扩展字段中,全部的延迟消息都保存在 ScheduleMessageService.SCHEDULE_TOPIC 的 Topic 中。
    • 备份原始 Topic、ququeId 为延迟消息的 Topic、ququeId。备份的目的是当消息到达投递时间时会恢复原始的 Topic 和 ququeId,继而被消费者拉取并消费。

  经过处理后,该消息会被正常保存到 CommitLog 中,然后创建 Consume Queue 和 Index File 两个索引。在创建 Consume Queue 时,从 CommitLog 中获取的消息内容会单独进行处理,单独处理的逻辑方法是 CommitLog.checkMessageAndReturnSize(),代码路径:D:\rocketmq-master\store\src\main\java\org\apache\rocketmq\store\CommitLog.java,具体代码如下:

 1                 // Timing message processing
 2                 {
 3                     String t = propertiesMap.get(MessageConst.PROPERTY_DELAY_TIME_LEVEL);
 4                     if (TopicValidator.RMQ_SYS_SCHEDULE_TOPIC.equals(topic) && t != null) {
 5                         int delayLevel = Integer.parseInt(t);
 6 
 7                         if (delayLevel > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
 8                             delayLevel = this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel();
 9                         }
10 
11                         if (delayLevel > 0) {
12                             tagsCode = this.defaultMessageStore.getScheduleMessageService().computeDeliverTimestamp(delayLevel,
13                                 storeTimestamp);
14                         }
15                     }
16                 }

  看上面这段代码,其中有一个很精巧的设计:在 CommitLog 中查询出消息后,调用 computeDeliverTimestamp() 方法计算消息具体的投递时间,再将时间保存在 ConsumeQueue 的 tagCode 中。这样设计的好处是,不需要检查 CommitLog 大文件,在定时任务检查消息是否需要投递时,只需检查 Consume Queue 中的tagCode(不再是消息 Tag 的 Hash 值,而是消息可以投递的时间,单位是 ms),如果满足条件再通过查询 CommitLog 将消息投递出去即可。如果每次都查询 CommitLog,那么可想而知,效率会很低。

  2.2 延迟消息投递机制

  RocketMQ 在存储延迟消息时,将其保存在一个系统的 Topic 中,在创建 Consume Queue 时,tagCode 字段中保存着延迟消息需要被投递的时间,通过这个存储实现的思路,我们总结一下延迟消息的投递过程:通过定时服务定时扫描 Consume Queue,满足投递时间条件的消息再通过查询 CommitLog 将消息重新投递到原始的 Topic 中,消费者就可以接收消息了。

  RocketMQ如何投递延迟消息给消费者呢?

  在存储模块初始化时,初始化延迟消息处理类 org.apache.rocketmq.store.schedule.ScheduleMessageService,通过依次调用 start() 方法来启动延迟消息定时扫描任务,代码路径:D:\rocketmq-master\store\src\main\java\org\apache\rocketmq\store\schedule\ScheduleMessageService.java,start() 方法的核心代码如下:

 1     public void start() {
 2         if (started.compareAndSet(false, true)) {
 3             this.timer = new Timer("ScheduleMessageTimerThread", true); #time:定时检查延迟消息是否可以投递的定时器。
 4             for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) { #该字段用于保存全部的延迟级别。
 5                 Integer level = entry.getKey();                         #延迟级别
 6                 Long timeDelay = entry.getValue();                      #延迟时间
 7                 Long offset = this.offsetTable.get(level);              #延迟级别对应的 Consume Queue 的消费位点,扫描时从这个位点开始。
 8                 if (null == offset) {
 9                     offset = 0L;
10                 }
11 
12                 if (timeDelay != null) {                               #参数表示延迟时间。
13                     this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
14                 }
15             }
16 
17             this.timer.scheduleAtFixedRate(new TimerTask() {
18 
19                 @Override
20                 public void run() {
21                     try {
22                         if (started.get()) ScheduleMessageService.this.persist();
23                     } catch (Throwable e) {
24                         log.error("scheduleAtFixedRate flush exception", e);
25                     }
26                 }
27             }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
28         }
29     }

  从代码中的 for 循环可以知道,每个延迟级别都有一个定时任务进行扫描,每个延迟级别在第一次扫描时会延迟 1000ms, 再开始执行扫描,以后每次扫描的时间间隔为 100ms。

  随着延迟消息不断被重新投递,内置 Topic 的全部 Consume Queue 的消费位点 offset 不断向前推进,也会定时执行 ScheduleMessageService.this.persist() 方法来持久化消费位点,以便进程重启后从上次开始扫描检查。

posted @ 2021-03-02 15:47  左扬  阅读(676)  评论(0编辑  收藏  举报
levels of contents