概述
? ???消息在發送到消息隊列RocketMQ版服務端后并不會立馬投遞,而是根據消息中的屬性延遲固定時間后才投遞給消費者。比如:常見的場景電商交易中超時未支付關閉訂單的場景,通過延時消息在30分鐘后投遞給消息端進行關單。
? ???開源 RocketMQ 針對目前只支持固定精度的定時消息。生產端發送消息,通過設delayTimeLevel時間級別后,可實現消息不立馬被消費者消費到,而是按照18個級別 ("1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";)。比如支持5秒、10秒的Level,那么用戶只能發送5秒延遲或者10秒延遲,不能發送8秒延遲的消息。
實現原理
Message轉化
- Producer端設置定時級別,發送延時消息后。
- Broker端收到延時消息時,會提前將延遲消息的 topic 進行轉化為進入 Topic 為 SCHEDULE_TOPIC_XXXX的消息,同時 延遲級別 與 消息隊列編號 做固定映射:QueueId = DelayLevel - 1。
if (msg.getDelayTimeLevel() > 0) {
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId())); msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
msg.setTopic(topic);
msg.setQueueId(queueId);
}
任務檢測
- 生成 ConsumeQueue 時,每條消息的 tagsCode 使用【消息計劃消費時間】。ScheduleMessageService 在輪詢 ConsumeQueue 時,可以使用 tagsCode 進行過濾。
- 對 SCHEDULE_TOPIC_XXXX 每條消費隊列(一共18個) broker端都會啟動了一個 timer和 timerTask的任務,默認1s執行一次 拉取數據,拉取過程 首先進行系統topic+delayLevel 查詢ConsumeQueue ,然后對比每條消息的延時時間和當前時間對比,發送 到達投遞時間【計劃消費時間】 的消息。將消息進行轉化并按照業務topic封裝好Message寫入到 commit_log 中。
DeliverDelayedMessageTimerTask#run
public void executeOnTimeup() {
ConsumeQueue cq =
ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
delayLevel2QueueId(delayLevel));
long failScheduleOffset = offset;
if (cq != null) {
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) {
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();
}
}
long now = System.currentTimeMillis();
long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
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) { // 將Message消息轉化并將其寫入
try {
MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
PutMessageResult putMessageResult =
ScheduleMessageService.this.writeMessageStore
.putMessage(msgInner);
if (putMessageResult != null
&& putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
continue;
}
} catch (Exception e) {
}
}
} else { // 安排下一次任務
ScheduleMessageService.this.timer.schedule(
new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
countdown);
ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
return;
}
}
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();
}
}
ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
failScheduleOffset), DELAY_FOR_A_WHILE);
}
- 當延時消息寫成功后,先更新延時消息消費進度(內存中),同時定時消息發送進度存儲在文件(../config/delayOffset.json)里,每 10s 定時持久化發送進度。
- 若發送延時消息失敗后,MQ會安排下一次任務繼續發送直到成功。
優缺點
優點:設計簡單,把所有相同延遲時間的消息都先放到一個隊列中,定時掃描,可以保證消息消費的有序性
缺點:定時器采用了timer,timer是單線程運行,如果延遲消息數量很大的情況下,可能單線程處理不過來,造成消息到期后也沒有發送出去的情況
總結
圖片.png