上一篇文章(19. consuemr消费失败后,消息如何处理?)分析到了,需要重新消费的消息,会被转存在延时队列中,这一篇文章,咱们就来深入分析一下延时队列
在rocketmq中,延时消息不支持自定义延时,只支持特定的延时时间级别
MessageStoreConfig
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
供18个延时时间,从1开始供18个级别,delayTimeLevel=0时,表示不延时。
在生产消息的时候,可以同个message.setDelayTimeLevel()来设置延时级别。
broker在收到消息之后,在commitlog.asyncPutMessage()方法里,会将原始的topic和queueId使用属性PROPERTY_REAL_TOPIC,PROPERTY_REAL_QUEUE_ID保存下来
然后将topic设置为SCHEDULE_TOPIC_XXXX,将queueId设置为delayTimeLevel-1,然后存储在commitlog中
等到reputService的时候,消息会被分到SCHEDULE_TOPIC_XXXX的consumeQueue中,并不会分配到目标topic的consumeQueue中,
接下来会通过ScheduleMessageService来调度延时消息
在DefaultMessageStore初始化的时候
public DefaultMessageStore(final MessageStoreConfig messageStoreConfig, final BrokerStatsManager brokerStatsManager,
final MessageArrivingListener messageArrivingListener, final BrokerConfig brokerConfig) throws IOException {
...
this.scheduleMessageService = new ScheduleMessageService(this);
...
}
DefaultMessageStore.start()
enableDLegerCommitLog默认是false
public void start() throws Exception {
...
if (!messageStoreConfig.isEnableDLegerCommitLog()) {
this.haService.start();
this.handleScheduleMessageService(messageStoreConfig.getBrokerRole());
}
...
}
public void handleScheduleMessageService(final BrokerRole brokerRole) {
if (this.scheduleMessageService != null) {
if (brokerRole == BrokerRole.SLAVE) {
this.scheduleMessageService.shutdown();
} else {
this.scheduleMessageService.start();
}
}
}
ScheduleMessageService.start()
public void start() {
// cas 设置started状态
if (started.compareAndSet(false, true)) {
super.load();
// 初始化一个timer
this.timer = new Timer("ScheduleMessageTimerThread", true);
for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
Integer level = entry.getKey();
Long timeDelay = entry.getValue();
// 初始化消费进度
Long offset = this.offsetTable.get(level);
if (null == offset) {
offset = 0L;
}
// 为每个延时级别都初始化一个timerTask
if (timeDelay != null) {
this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
}
}
// 定时任务,默认每隔10s持久化一下延时队列的消费进度
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
if (started.get()) ScheduleMessageService.this.persist();
} catch (Throwable e) {
log.error("scheduleAtFixedRate flush exception", e);
}
}
}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
}
}
delayLevelTable就是18个定时级别了,先初始化一个timer,然后为每个delayLevel都会初始化一个延时任务(DeliverDelayedMessageTimerTask),延时1s,
然后有初始化了一个定时任务,定期持久化一下延时队列的消费进度
org.apache.rocketmq.store.schedule.ScheduleMessageService.DeliverDelayedMessageTimerTask#run
public void run() {
try {
if (isStarted()) {
this.executeOnTimeup();
}
} catch (Exception e) {
// XXX: warn and notify me
log.error("ScheduleMessageService, executeOnTimeup exception", e);
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
this.delayLevel, this.offset), DELAY_FOR_A_PERIOD);
}
}
public void executeOnTimeup() {
// 获取到consumeQueue
ConsumeQueue cq =
ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
delayLevel2QueueId(delayLevel));
long failScheduleOffset = offset;
if (cq != null) {
//读取到buffer
SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
if (bufferCQ != null) {
try {
long nextOffset = offset;
int i = 0;
ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
// 每次读取一个消息
// commitlog offset
long offsetPy = bufferCQ.getByteBuffer().getLong();
// msg size
int sizePy = bufferCQ.getByteBuffer().getInt();
// tagscode
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);
tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
}
}
long now = System.currentTimeMillis();
//送达时间?
long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
// 计算下一个offset
nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
long countdown = deliverTimestamp - now;
// 判断消息到期没有,到期了就读取消息的完整内容
if (countdown <= 0) {
// 读取消息的完整内容
MessageExt msgExt =
ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
offsetPy, sizePy);
if (msgExt != null) {
try {
// 重新构建消息
// 如果是普通的消息,会恢复成原来的topic和queueId
// 如果是重试消息,就恢复成重试的topic和queueId,在消费之前才会恢复成最原始的topic和queueId
MessageExtBrokerInner msgInner = 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;
}
// 将消息写入到commitlog
PutMessageResult putMessageResult =
ScheduleMessageService.this.writeMessageStore
.putMessage(msgInner);
// 写入成功
if (putMessageResult != null
&& putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
if (ScheduleMessageService.this.defaultMessageStore.getMessageStoreConfig().isEnableScheduleMessageStats()) {
ScheduleMessageService.this.defaultMessageStore.getBrokerStatsManager().incTopicPutNums(msgInner.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum(), 1);
ScheduleMessageService.this.defaultMessageStore.getBrokerStatsManager().incTopicPutSize(msgInner.getTopic(),
putMessageResult.getAppendMessageResult().getWroteBytes());
ScheduleMessageService.this.defaultMessageStore.getBrokerStatsManager().incBrokerPutNums(putMessageResult.getAppendMessageResult().getMsgNum());
}
continue;
} else {
// XXX: warn and notify me
log.error(
"ScheduleMessageService, a message time up, but reput it failed, topic: {} msgId {}",
msgExt.getTopic(), msgExt.getMsgId());
// 写入commitlog失败,为下一个offset生成一个timertask,并更新消费进度
ScheduleMessageService.this.timer.schedule(
new DeliverDelayedMessageTimerTask(this.delayLevel,
nextOffset), DELAY_FOR_A_PERIOD);
ScheduleMessageService.this.updateOffset(this.delayLevel,
nextOffset);
return;
}
} catch (Exception e) {
/*
* XXX: warn and notify me
*/
log.error(
"ScheduleMessageService, messageTimeup execute error, drop it. msgExt="
+ msgExt + ", nextOffset=" + nextOffset + ",offsetPy="
+ offsetPy + ",sizePy=" + sizePy, e);
}
}
} else {
// 为下一个offset生成1个timertask,并更新进度
ScheduleMessageService.this.timer.schedule(
new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
countdown);
ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
return;
}
} // end of for
nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);
ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
return;
} finally {
bufferCQ.release();
}
} // end of if (bufferCQ != null)
else {
long cqMinOffset = cq.getMinOffsetInQueue();
if (offset < cqMinOffset) {
failScheduleOffset = cqMinOffset;
log.error("schedule CQ offset invalid. offset=" + offset + ", cqMinOffset="
+ cqMinOffset + ", queueId=" + cq.getQueueId());
}
}
} // end of if (cq != null)
// 没有找到消费队列,就表示还没有重试消息或者延时消息,创建timertask, 100ms后重新调度
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
failScheduleOffset), DELAY_FOR_A_WHILE);
}
先根据topic:SCHEDULE_TOPIC_XXXX、queueId=delayLevel,定位动consumeQueue, 再根据消费offset从consumeQueue中读取未消费的消息,这里读取的消息只是consumeQueue中的消息(commitlog offset, msg size, tagccode),并不是完整的消息
接着校正消息的实际送达时间,然后判断消息是否到期了,到期了就从commitlog中读取完整的消息。
然后将消息重新封装,恢复原来的topic和queueId,重新投递到commitlog中,在经reputService分配到相应的consumeQueue中,这样消费者就可以消费啦
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
接着分析上篇文章(19. consuemr消费失败后,消息如何处理?)中的,消息消费失败之后,会生成重试消息发送到broker中,然后broker将其转成延时消息(延时级别=3+重试次数)存储在commitlog中,reputservice会将其分配到topic:SCHEDULE_TOPIC_XXXX、queueId=delayLevel的consumeQueue中,这时consumer就可以再次消费消息了。