RocketMQ延迟消息实现原理
RocketMQ的延迟消息并非在用户指定的精确时间点才投递,而是通过一个“等级”机制,将消息暂存到不同的内部队列中,由定时任务在到达预定延迟等级的时间后,才将其投递到目标 Topic 供消费者消费。
延迟等级(Delay Level)
RocketMQ 不支持任意时间的延迟,而是预设了 18 个延迟等级(Delay Level)。每个等级对应一个固定的延迟时间。发送时:生产者设置的是 delayLevel
内部 Topic:SCHEDULE_TOPIC_XXXX
这是实现延迟消息的核心。RocketMQ 在 Broker 内部为延迟消息创建了一个特殊的 Topic,名字格式为 SCHEDULE_TOPIC_XXXX。
队列映射:这个内部 Topic 默认包含 18 个队列(queueId从 0 到 17),每个队列严格对应一个延迟等级。
queueId = 0对应 delayLevel = 1(延迟 1s)
queueId = 1对应 delayLevel = 2(延迟 5s)
…
queueId = 17对应 delayLevel = 18(延迟 2h)
消息的“改头换面”与存储
当 Broker 接收到一条延迟消息时,它不会将其存入目标 Topic(如 YourTargetTopic)的队列中,而是会执行以下操作:
修改消息属性:
将消息的真实 Topic(如 YourTargetTopic)和真实 queueId 保存到消息的属性中。
将消息的 Topic 替换为内部 Topic:SCHEDULE_TOPIC_XXXX。
根据生产者设置的 delayLevel,计算出对应的 queueId,并将消息存入这个内部队列。
记录消息的投递时间:投递时间 = 消息存储时间(storeTimestamp) + 延迟等级对应的时间。
举例
一条要发送到 OrderTopic的消息,设置了 delayLevel=4(延迟30秒)。
Broker 收到后,会将其真实信息 Real Topic: OrderTopic, Real QueueId: 1存入属性。
然后将其 Topic 改为 SCHEDULE_TOPIC_XXXX,QueueId 改为 3(因为 delayLevel=4对应 queueId=3)。
最后将消息存入 SCHEDULE_TOPIC_XXXX的 queueId=3这个队列中。
定时任务扫描与投递
Broker 启动时,会为每一个延迟级别启动一个定时任务(TimerTask)。
任务执行:每个定时任务以 1 秒为周期,扫描自己负责的那个延迟等级队列。
检查逻辑:定时任务会检查当前队列中的消息。它依据的是 Offset,而不是直接遍历消息内容。它会取出队列中最早的消息,判断其“投递时间”是否已到。
如果 当前时间 >= 投递时间,说明这条消息需要被投递了。
如果 当前时间 < 投递时间,说明这个队列里所有的消息都还没到时间(因为消息是顺序存储的),任务会等待下一次扫描。
重新投递:对于到期的消息,定时任务会:
从消息属性中恢复其真实的 Topic 和 QueueId。
将这条消息重新写入 Commit Log,但这次的目标是真实的 Topic 队列。
这样,这条消息就对消费者可见了,消费者可以像消费普通消息一样拉取到它。
同时,原延迟消息(在 SCHEDULE_TOPIC_XXXX中的那条)会被视为已处理。
关键特点与注意事项
非精确任意延迟:最大的特点是使用固定等级,而非任意时间。这在很多业务场景下(如超时关单)是足够用的,且实现简单高效。
高可靠性:延迟消息和普通消息一样,会持久化到 Commit Log,并支持主从复制,因此 Broker 重启不会丢失。
投递延迟:由于定时任务是每秒扫描一次,所以消息的投递会有最多 1 秒的额外延迟。这属于“秒级精度”。
自定义延迟等级:你可以通过修改 Broker 的配置文件来自定义延迟等级。
与普通消息无差别消费:对于消费者来说,延迟消息和普通消息没有任何区别,它感知不到延迟过程,这简化了消费端的逻辑。
1121

被折叠的 条评论
为什么被折叠?



