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() 方法来持久化消费位点,以便进程重启后从上次开始扫描检查。