RocketMQ延迟消息

延迟消息介绍

延迟队列表示生产的消息发送到服务端后,并不能立刻被消费者消费,等到到达消息的延迟时间后才会被消费。

场景案例:用户下了一个订单之后,需要在指定时间内(例如30分钟)进行支付,在到期之前可以发送一个消息提醒用户进行支付。

一些消息中间件的Broker端内置了延迟消息支持的能力,如:

  • RabbitMQ:需要安装一个rabbitmq_delayed_message_exchange插件。
  • RocketMQ:RocketMQ 开源版本延迟消息临时存储在一个内部主题中,不支持任意时间精度,支持特定的 level,例如定时 5s,10s,1m 等。
  • Pulsar:Pulsar使用Delayed Message Tracker为延迟消息记录对应的index,并建立delayed index优先级队列。

Broker端内置延迟消息处理能力,核心实现思路都是一样的,将延迟消息通过一个临时存储进行暂存,到期后才投递到目标topic中。如下图所示:

在这里插入图片描述

其中,broker内部通过一个延迟服务检查消息是否到期,将到期的消息投递到目标topic中,这是对业务透明的。图中的延迟服务名字为delay service,不同消息中间件的延迟服务模块名称可能不同。

显然,临时存储模块和延迟服务模块,是延迟消息实现的关键。

rocketmq中的延迟消息

开源RocketMQ支持延迟消息,但是不支持秒级精度。默认支持18个level的延迟消息,这是通过broker端的messageDelayLevel配置项确定的,如下:

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

发消息时,设置delayLevel等级即可:msg.setDelayLevel(level)。level有以下三种情况:

  • level == 0,消息为非延迟消息
  • 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
  • level > maxLevel,则level== maxLevel,例如level==20,延迟2h

定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。

延迟级别的值可以进行修改,以满足自己的业务需求,可以修改/添加新的level。例如:你想支持2天的延迟,修改最后一个level的值为2d,这个时候依然是18个level;也可以增加一个2d,这个时候总共就有19个level。

生产者发送延迟消息:

生产者在发送延迟消息非常简单,只需要设置一个延迟级别即可,注意不是具体的延迟时间,如:

Message msg=new Message();
msg.setTopic("TopicA");
msg.setTags("Tag");
msg.setBody("this is a delay message".getBytes());
//设置延迟level为5,对应延迟1分钟
msg.setDelayTimeLevel(5);
producer.send(msg);

延迟消息在RocketMQ Broker端的流转如下图所示:

在这里插入图片描述

说明如下:

1、produer判断消息是延迟消息,修改消息的topic名称和队列信息。RocketMQ Broker端在存储生产者写入的消息时,首先都会将其写入到CommitLog中。之后根据消息中的Topic信息和队列信息,将其转发到目标Topic的指定队列(ConsumeQueue)中。

由于消息一旦存储到ConsumeQueue中,消费者就能消费到,而延迟消息不能被立即消费,所以这里将Topic的名称修改为SCHEDULE_TOPIC_XXXX,并根据延迟级别确定要投递到哪个队列下。

同时,还会将消息原来要发送到的目标Topic和队列信息存储到消息的属性中。

相关源码见org.apache.rocketmq.store.CommitLog#putMessage。

2、转发消息到SCHEDULE_TOPIC_XXXX的延迟主题的ConsumerQueue中。

CommitLog中的消息转发到CosumeQueue中是异步进行的。在转发过程中,会对延迟消息进行特殊处理,主要是计算这条延迟消息需要在什么时候进行投递。

投递时间=消息存储时间(storeTimestamp) + 延迟级别对应的时间

3、延迟服务消费SCHEDULE_TOPIC_XXXX消息。

Broker内部有一个ScheduleMessageService类,其充当延迟服务,消费SCHEDULE_TOPIC_XXXX中的消息,并投递到目标Topic中。

ScheduleMessageService在启动时,其会创建一个定时器Timer,并根据延迟级别的个数,启动对应数量的TimerTask,每个TimerTask负责一个延迟级别的消费与投递。

相关源码见ScheduleMessageService#start。

4、将消息重新存储到Commitlog中。

在将消息到期后,需要投递到目标Topic。由于在第一步已经记录了原来的Topic和队列信息,因此这里重新设置,再存储到CommitLog即可。此外,由于之前Message Tag HashCode字段存储的是消息的投递时间,这里需要重新计算tag的哈希值后再存储。

源码见:DeliverDelayedMessageTimerTask#messageTimeup。

5、将消息投递到目标topic;

这一步与第二步类似,不过由于消息的Topic名称已经改为了目标Topic。因此消息会直接投递到目标Topic的ConsumeQueue中,之后消费者即消费到这条消息。

6、consumer消费目标topic中的数据;

延迟消息与消息重试的关系

RocketMQ提供了消息重试的能力,在并发模式消费消费失败的情况下,可以返回一个枚举值RECONSUME_LATER,那么消息之后将会进行重试。如:

consumer.registerMessageListener(new MessageListenerConcurrently() {
       @Override
       public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                       ConsumeConcurrentlyContext context) {
           //处理消息,失败,返回RECONSUME_LATER,进行重试
           return ConsumeConcurrentlyStatus.RECONSUME_LATER;
       }
   });

重试默认会进行重试16次。使用过RocketMQ消息重试功能的用户,可能看到过以下这张图:

第几次重试与上次重试的间隔时间第几次重试与上次重试的间隔时间
110 秒97 分钟
230 秒108 分钟
31 分钟119 分钟
42 分钟1210 分钟
53 分钟1320 分钟
64 分钟1430 分钟
75 分钟151 小时
86 分钟162 小时

事实上,RocketMQ的消息重试也是基于延迟消息来完成的。在消息消费失败的情况下,将其重新当做延迟消息投递回Broker。在投递回去时,会跳过前两个level,因此只重试16次。

自定义延迟队列

此外,还有一些mq中间件原生不支持延迟消息,如kafka。这种情况下, 可以对kafka进行改造,需要修改kafka内核的代码,这种方式成本较大。另外一种方式使用第三方临时存储,并加一层延迟消息的存取转发服务。

第三方存储选型要求:

对于第三方临时存储,其需要满足以下几个特点:

  • 高性能:读写延迟要低
  • 高可靠:消息写入后不能丢失,需要进行持久化和备份
  • 支持排序:支持按照某个字段对消息进行排序,对于延迟消息需要按照时间进行排序。普通消息通常先发送的会被先消费,延迟消息与普通消息不同,需要进行排序。例如先发一条延迟10s的消息,再发一条延迟5s的消息,那么后发送的消息需要被先消费。
  • 支持长时间保存:一些业务的延迟消息,需要延迟1个月甚至更长时间,所以延迟消息必须能长时间保留。

例如,滴滴开源的消息中间件DDMQ,在底层消息中间件的基础上加了一层代理和独立的延迟服务模块,并使用RocksDB进行临时存储。

DDMQ解决方案的延迟消息流转如下图所以:

在这里插入图片描述

说明如下:

  1. 生产者将发送给producer proxy,proxy判断是延迟消息,将其投递到一个缓冲Topic中。
  2. delay service启动消费者,用于从缓冲topic中消费延迟消息,以时间为key,存储到rocksdb中。
  3. delay service判断消息到期后,将其投递到目标Topic中。
  4. 消费者消费目标topic中的数据。

这种方式的好处是,因为delay service的延迟投递能力是独立于broker实现的,不需要对broker做任何改造,对于任意的MQ类型都可以提供支持延迟消息的能力。其实DDMQ借助于proxy和delay service不仅对RocketMQ提供了秒级精度的延迟消息投递能力,对Kafka也提供了这种能力。

事实上,DDMQ还提供了很多其他的功能,仅仅从延迟消息的角度,完全没必要使用这个proxy,直接将消息投递到缓冲topic中,然后通过delay service完成延迟投递功能即可。

具体到delay service模块上,有一些重要的细节:

1、保证服务高可用,delay service也需要部署多个节点。

2、为了保证数据不丢失,每个delay service节点都需要消费缓冲topic中的全量数据,保存到各自的持久化存储中,这样就有了多个备份。不过因为是各自拉取,并不能保证强一致。如果要强一致,那么delay service就不需要内置存储实现,可以借助于其他支持强一致的存储,delay service只负责写入和查询。

3、为了避免重复投递,delay service需要进行选主,可以借助于zookeeper、etcd等实现。只有master可以投递到目标topic中,其他节点处于备用状态。或者master根据目标topic做消息分区,控制delay service实例投递不同到目标topic消息。

4、master要记录自己当前投递到的时间到一个共享存储中,如果master挂了,从slave节点中选出一个新的master节点,从之前记录时间继续开始投递。

5、延迟消息取消:一些延迟消息在未到期之前,可能希望进行取消。通常取消逻辑实现较为复杂,且不够精准。对于那些快到期的消息,可能还取消之前,已经发送出去了,因此需要在消费者端再做检查,才能万无一失。

6、延迟消息的状态有初始化、等待投递、投递成功、投递失败、投递重试、取消投递等。

参考

https://cloud.tencent.com/developer/article/1581368

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值