什么是延时消息?
指的是当消息写入到Broker后,不能立刻被消费者消费,需要等待指定的时长后才可被消费处理的消息。
延时消息等级
RocketMQ延时消息的延迟时长不支持随意时长的延迟。
- 是通过特定的延迟等级来指定的。
默认支持18个等级的延迟消息。
延时等级定义在RocketMQ服务端的MessageStoreConfig类中的如下变量中:
// MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
level 有以下三种情况:
- level == 0,消息为非延迟消息。
- 1<=level<=maxLevel,消息延迟特定时间,例如:level==1,延迟1s。
- level > maxLevel,则level== maxLevel,例如level==20,延迟2h。
发消息时,设置delayLevel等级即可:msg.setDelayLevel(level)。
例如指定的延时等级为3,则表示延迟时长为10s,延迟等级是从1开始计数的。
使用场景
1、电商交易系统的订单超时未支付,自动取消订单。
2、超时自动审批,系统审批流程可以设置为超过设定时间后自动执行通过或者拒绝流程。
3、限时优惠活动,商品需要促销,在活动开始时,发送一个两小时后触发的定时消息,用于在活动结束时恢复原价。
为什么不支持任意时间
按照《RocketMQ Developer Guide》中的说法:
- 如果提供任意时间,就会涉及到消息的排序,会有一定的性能损耗。
而RocketMQ这种利用固定延迟级别到单个队列的实现方式是一种妥协,灵活性和极致性能的妥协。
延迟消息与消费重试的关系
消息重试的16个级别,实际上是把延迟消息18个级别的前两个Level去掉了。
事实上,RocketMQ的消息重试也是基于延迟消息来完成的。
- 在消息消费失败的情况下,将其重新当做延迟消息投递回Broker。
在投递回去时,会跳过前两个Level,因此只重试16次。
详细内容可以看我之前的文章!!!
实现原理
RocketMQ延时消息会暂存在名为SCHEDULE_TOPIC_XXXX的Topic中。
- 并根据delayTimeLevel存入特定的Queue。
queueId = delayTimeLevel – 1:即一个Queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。
Broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的Topic。
图片
主要步骤:
图片
修改消息Topic名称和队列信息:
RocketMQ Broker端在存储生产者写入的消息时,首先都会将其写入到CommitLog中。
之后根据消息中的Topic信息和队列信息,将其转发到目标Topic的指定队列(ConsumeQueue)中。
由于消息一旦存储到ConsumeQueue中,消费者就能消费到,而延迟消息不能被立即消费。
所以将Topic的名称修改为SCHEDULE_TOPIC_XXXX,并根据延迟级别确定要投递到哪个队列下。
- 同时,还会将消息原来要发送到的目标Topic和队列信息存储到消息的属性中。
转发消息到延迟主题SCHEDULE_TOPIC_XXXX的CosumeQueue中:
CommitLog中的消息转发到CosumeQueue中是异步进行的。
在转发过程中,会对延迟消息进行特殊处理,主要是计算这条延迟消息需要在什么时候进行投递。
- 投递时间 = 消息存储时间(StoreTimestamp) + 延迟级别对应的时间。
图片
延迟服务消费SCHEDULE_TOPIC_XXXX消息:
Broker内部有一个ScheduleMessageService类,其充当延迟服务。
- 主要是消费SCHEDULE_TOPIC_XXXX中的消息,并投递到目标Topic中。
ScheduleMessageService在启动时,其会创建一个定时器Timer,并根据延迟级别的个数,启动对应数量的TimerTask。
- 每个TimerTask负责一个延迟级别的消费与投递。
如果可以投放,则在投放到原本的目的Topic。
每隔100ms,从TopicSCHEDULE_TOPIC_XXXX判断18个队列里的第一个消息是否可以被投放。
需要注意
每个TimeTask在检查消息是否到期时:
- 首先检查对应队列中尚未投递第一条消息。
- 如果这条消息没到期,那么之后的消息都不会检查。
- 如果到期了,则进行投递,并检查之后的消息是否到期。
图片
图片
将信息重新存储到CommitLog中:
在将消息到期后,需要投递到目标Topic。
由于在第一步已经记录了原来的Topic和队列信息,因此这里重新设置,再存储到CommitLog即可。
将消息投递到目标Topic中:
由于消息
的Topic名称已经改为了目标Topic。
因此消息会直接投递到目标Topic的ConsumeQueue中,之后消费者即消费到这条消息。
消费者消费目标Topic中的数据。