RIP-43 延迟消息

延迟消息是什么

不会立即发送,而是在指定时间间隔后才发送出去。
RocketMQ5.0支持任意级别的延迟消息

延迟消息怎么用

发送的时候指定时间戳,需要使用5.0.0以上的客户端

// This message will be delivered to consumer 10 seconds later.
message.setDelayTimeSec(10);

// The effect is the same as the above
message.setDelayTimeMs(10_000L);

// Set the specific delivery time, and the effect is the same as the above
message.setDeliverTimeMs(System.currentTimeMillis() + 10_000L);

实现原理

借用网图

定时消息写入CommitLo之前,会将Topic替换为系统Topic(rmq_sys_wheel_timer),然后TimerEnqueueGetService和TimerEnqueuePutService会将消息写入时间轮
延迟时间到达之后,TimerDequeueGetService、TimerDequeueGetMessageService和TimerDequeuePutMessageService会将消息重发回CommitLog中,并将Topic替换为真实Topic
如果延迟时间超出了时间轮的范围呢?
时间轮的每一格代表一个时间刻度,超过时间轮范围的消息仍然会被放到时间轮中,就像是数组取模操作,因此,如果出时间轮时时间未到期,会再次执行写CommitLog -> 入时间轮-> 出时间轮的操作

源码

代码为5.0.1版本

1. 写CommitLog

HookUtils#transformTimerMessage() 校验是否为定时消息,并替换Topic,随后与正常的逻辑一样
生成ConsumerQueue与IndexFile

2. TimerEnqueueGetService

TimerMessageStore#enqueue()
逻辑就是不断根据ConsumeQueue查找消息,把消息的Offset,delayTime,size,msgExt封装成TimerRequest请求,加入enqueuePutQueue中

ConsumeQueue cq = (ConsumeQueue) this.messageStore.getConsumeQueue(TIMER_TOPIC, queueId);
        if (null == cq) {
            return false;
        }
        if (currQueueOffset < cq.getMinOffsetInQueue()) {
            LOGGER.warn("Timer currQueueOffset:{} is smaller than minOffsetInQueue:{}", currQueueOffset, cq.getMinOffsetInQueue());
            currQueueOffset = cq.getMinOffsetInQueue();
        }
        long offset = currQueueOffset;
        SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(offset);
        if (null == bufferCQ) {
            return false;
        }
        try {
            int i = 0;
            for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
                perfs.startTick("enqueue_get");
                try {
                    long offsetPy = bufferCQ.getByteBuffer().getLong();
                    int sizePy = bufferCQ.getByteBuffer().getInt();
                    bufferCQ.getByteBuffer().getLong(); //tags code
                    MessageExt msgExt = getMessageByCommitOffset(offsetPy, sizePy);
                    if (null == msgExt) {
                        perfs.getCounter("enqueue_get_miss");
                    } else {
                        lastEnqueueButExpiredTime = System.currentTimeMillis();
                        lastEnqueueButExpiredStoreTime = msgExt.getStoreTimestamp();
                        long delayedTime = Long.parseLong(msgExt.getProperty(TIMER_OUT_MS));
                        // use CQ offset, not offset in Message
                        msgExt.setQueueOffset(offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE));
                        TimerRequest timerRequest = new TimerRequest(offsetPy, sizePy, delayedTime, System.currentTimeMillis(), MAGIC_DEFAULT, msgExt);
                        while (true) {
                            if (enqueuePutQueue.offer(timerRequest, 3, TimeUnit.SECONDS)) {
                                break;
                            }
                            if (!isRunningEnqueue()) {
                                return false;
                            }
                        }
                    }
                } catch (Exception e) {
                    //here may cause the message loss
                    if (storeConfig.isTimerSkipUnknownError()) {
                        LOGGER.warn("Unknown error in skipped in enqueuing", e);
                    } else {
                        holdMomentForUnknownError();
                        throw e;
                    }
                } finally {
                    perfs.endTick("enqueue_get");
                }
                //if broker role changes, ignore last enqueue
                if (!isRunningEnqueue()) {
                    return false;
                }
                currQueueOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
            }
            currQueueOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
            return i > 0;
        } catch (Exception e) {
            LOGGER.error("Unknown exception in enqueuing", e);
        } finally {
            bufferCQ.release();
        }

3. TimerEnqueuePutService

从队列中获取请求,将10个组成一组,对于每一个TimerRequest,先写入timerLog文件中,再写入时间轮的对应的slot中
timerLog和CommitLog类似,但是是用来保存定时消息的各项数据的,每一项由以下几项组成
image
字段 大小 含义
size 4B 保存记录的大小
prev pos 8B 上一个位置
magic value 4B 魔法值,标识这条消息需要滚动或删除
curr write time 8B 写入timerLog的时间戳
delayed time 4B 剩余延迟时间
offsetPhy 8B 延迟消息在CommitLog中的物理位点
sizePy 4B 消息的大小
hashCode 4B 真实topic的哈希值
reserved value 8B 保留值

时间轮的每个slot组成为
image
分别表示投递时间,slot中第一条消息offset,最后一条消息的offset,slot中的数目(用来做流控),magic(未用到)

消息索引写入到timerLog中是顺序写的,但是同时维护了上一条消息的索引,因此可以根据最后一条消息遍历所有消息
image

4. TimerDequeueGetService

时间到期后,TimerDequeueGetService#dequeue()会遍历当前时间戳对应slot中的所有消息并处理,
根据slot的lastPos可以在tiemrLog中找到对应的消息的offset,并可以根据prePos向前遍历
timerLog中如果magic为MAGIC_DELETE,表示这是一条用来取消投递另一条消息的消息,这里不谈
这里同样做了一个分组处理,目的是先处理撤销逻辑,deleteList,再处理normalList,达到取消逻辑
将请求加入到dequeueGetQueue中

5. TimerDequeueGetMessageService

这里做了撤销的逻辑,先将deleteList中的msgId获取到,然后匹配normalList中的msg,没有匹配到的表示不撤销,加入到timerPutQueue中
image

6. TiemrDequeuePutMessagseService

就是判断是否需要滚动投递,如果要就添加ROLL_TIMES属性
然后doput写入CommitLog中
image

怎么保证高可用

timerLog以及timerWheel都有持久化机制,宕机时
timerLog

public void flush() {
	ByteBuffer bf = localBuffer.get();
	bf.position(0);
	bf.limit(wheelLength);
	mappedByteBuffer.position(0);
	mappedByteBuffer.limit(wheelLength);
	for (int i = 0; i < wheelLength; i++) {
		if (bf.get(i) != mappedByteBuffer.get(i)) {
			mappedByteBuffer.put(i, bf.get(i));
		}
	}
	this.mappedByteBuffer.force();
}

每秒钟持久化一次,10s打印一次日志
image

当服务宕机时会发生什么?

改进空间?

posted @ 2022-10-16 16:46  风卷红旗过大江  阅读(298)  评论(0)    收藏  举报