携手创作,共同成长!这是我参与「掘金日新计划 · 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参数需要设置成当前时间戳之后的某个时刻(单位毫秒)。如果被设置成当前时间戳之前的某个时刻,消息将立刻投递给消费者。
- 由于客户端和服务端可能存在时间差,消息的实际投递时间与客户端设置的投递时间之间可能存在偏差。
定时消息源码解析
对照源码与流程图进行理解
流程图
源码如下:
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
的理解通过看源码、查网上资料、画流程图这些方法,其中不免有很多地方理解错误,希望看到的小伙伴能一起交流,共同进步~