上一篇文章介绍了RabbitMQ的基本使用,这篇文章总结RabbitMQ的高级使用方法
1.TTL(Time o To Live) 消息过期时间
有两种设置方式
- 通过队列属性设置消息过期时间
通过队列属性设置消息过期时间所有队列中的消息超过时间未被消费时,都会过期。
@Bean("ttlQueue")
public Queue queue() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("x-message-ttl", 11000); // 队列中的消息未被消费 11 秒后过期
return new Queue("TTL_QUEUE", true, false, false, map);
}
- 设置单条消息的过期时间
在发送消息的时候指定消息属性。
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("4000"); // 消息的过期属性,单位 ms
Message message = new Message("这条消息 4 秒后过期".getBytes(), messageProperties);
rabbitTemplate.send("TTL_EXCHANGE", "bread.ttl", message);
如果同时指定了 Message TTL 和 Queue TTL,则小的那个时间生效。
2.死信队列
消息在某些情况下会变成死信 (Dead Letter) 。
队列在创建的时候可以指定一个死信交换机 DLX(Dead Letter Exchange)。
死信交换机绑定的队列被称为死信队列 DLQ(Dead Letter Queue),DLX 实际上
也是普通的交换机,DLQ 也是普通的队列(例如替补球员也是普通球员)。
什么情况下消息会变成死信?
- 消息被消费者拒绝并且未设置重回队列:(NACK || Reject ) && requeue== false
- 消息过期
- 队列达到最大长度,超过了 Max length(消息数)或者 Max length bytes(字节数),最先入队的消息会被发送到 DLX。
死信队列如何使用?
- 声明原交换机(GP_ORI_USE_EXCHANGE)、原队列(GP_ORI_USE_QUEUE),相互绑定。队列中的消息 10 秒钟过期,因为没有消费者,会变成死信。指定原队列的死信交换
机(GP_DEAD_LETTER_EXCHANGE)。
@Bean("oriUseExchange")
public DirectExchange exchange() {
return new DirectExchange("ORI_USE_EXCHANGE", true, false, new HashMap<>());
}
@Bean("oriUseQueue")
public Queue queue() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("x-message-ttl", 10000); // 10 秒钟后成为死信
map.put("x-dead-letter-exchange", "DEAD_LETTER_EXCHANGE"); // 队列中的消息变成死信后,进入死信交换机
return new Queue("ORI_USE_QUEUE", true, false, false, map);
}
@Bean
public Binding binding(@Qualifier("oriUseQueue") Queue queue,@Qualifier("oriUseExchange") DirectExchange
exchange) {
return BindingBuilder.bind(queue).to(exchange).with("ori.use");
}
- 声明死信交换机 ( DEAD_LETTER_EXCHANGE ) 、 死信队列(DEAD_LETTER_QUEUE),相互绑定
@Bean("deatLetterExchange")
public TopicExchange deadLetterExchange() {
return new TopicExchange("DEAD_LETTER_EXCHANGE", true, false, new HashMap<>());
}
@Bean("deatLetterQueue")
public Queue deadLetterQueue() {
return new Queue("DEAD_LETTER_QUEUE", true, false, false, new HashMap<>());
}
@Bean
public Binding bindingDead(@Qualifier("deatLetterQueue") Queue queue,@Qualifier("deatLetterExchange")
TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("#"); // 无条件路由
}
- 最终消费者监听死信队列。
- 生产者发送消息。
示意图
3. 延迟队列
我们在实际业务中有一些需要延时发送消息的场景,例如:
- 家里有一台智能热水器,需要在 30 分钟后启动
- 未付款的订单,15 分钟后关闭
RabbitMQ 本身不支持延迟队列,总的来说有三种实现方案:
- 先存储到数据库,用定时任务扫描
- 利用 RabbitMQ 的死信队列(Dead Letter Queue)实现
- 利用 rabbitmq-delayed-message-exchange 插件
1.TTL+DLX实现延迟队列
流程上面代码和图示已经讲过了
消息的流转流程:
生产者——原交换机——原队列(超过 TTL 之后)——死信交换机——死信队列—
—最终消费者
使用死信队列实现延时消息的缺点:
- 如果统一用队列来设置消息的 TTL,当梯度非常多的情况下,比如 1 分钟,2分钟,5 分钟,10 分钟,20 分钟,30 分钟……需要创建很多交换机和队列来路由消息。
- 如果单独设置消息的 TTL,则可能会造成队列中的消息阻塞——前一条消息没有出队(没有被消费),后面的消息无法投递(比如第一条消息过期 TTL 是 30min,第二条消息 TTL 是 10min。10 分钟后,即使第二条消息应该投递了,但是由于第一条消息还未出队,所以无法投递)。
- 可能存在一定的时间误差。
2. 基于延迟队列插件的实现(Linux)
在 RabbitMQ 3.5.7 及 以 后 的 版 本 提 供 了 一 个 插 件(rabbitmq-delayed-message-exchange)来实现延时队列功能。同时插件依赖Erlang/OPT 18.0 及以上。
插件源码地址:
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
插件下载地址:
https://bintray.com/rabbitmq/community-plugins/rabbitmq_delayed_message_exchange
- 进入插件目录
whereis rabbitmq
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.12/plugins
- 下载插件
wget
https://bintray.com/rabbitmq/community-plugins/download_file?file_path=rabbitmq_delayed_message_exchange-0.0.1.e
z
如果下载的文件名带问号则需要改名
mv download_file?file_path=rabbitmq_delayed_message_exchange-0.0.1.ez
rabbitmq_delayed_message_exchange-0.0.1.ez
- 启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 停用插件
rabbitmq-plugins disable rabbitmq_delayed_message_exchange
- 插件使用
通过声明一个 x-delayed-message 类型的 Exchange 来使用 delayed-messaging特性。x-delayed-message 是插件提供的类型,并不是 rabbitmq 本身的(区别于 direct、topic、fanout、headers)
@Bean("delayExchange")
public TopicExchange exchange() {
Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-delayed-type", "direct");
return new TopicExchange("DELAY_EXCHANGE", true, false, argss);
}
生产者:
消息属性中指定 x-delay 参数。
MessageProperties messageProperties = new MessageProperties();
// 延迟的间隔时间,目标时刻减去当前时刻
messageProperties.setHeader("x-delay", delayTime.getTime() - now.getTime());
Message message = new Message(msg.getBytes(), messageProperties);
// 不能在本地测试,必须发送消息到安装了插件的 Linux 服务端
rabbitTemplate.send("DELAY_EXCHANGE", "#", message);
4. 服务端流控(Flow Control)
当 RabbitMQ 生产 MQ 消息的速度远大于消费消息的速度时,会产生大量的消息堆积,占用系统资源,导致机器的性能下降。我们想要控制服务端接收的消息的数量,应该怎么做呢?队列有两个控制长度的属性:
- x-max-length:队列中最大存储最大消息数,超过这个数量,队头的消息会被丢弃。
- x-max-length-bytes:队列中存储的最大消息容量(单位 bytes),超过这个容量,队头的消息会被丢弃。
需要注意的是,设置队列长度只在消息堆积的情况下有意义,而且会删除先入队的消息,不能真正地实现服务端限流。
有没有其他办法实现服务端限流呢?
4.1 内存控制
RabbitMQ 会在启动时检测机器的物理内存数值。默认当 MQ 占用 40% 以上内存时,MQ 会主动抛出一个内存警告并阻塞所有连接(Connections)。可以通过修改rabbitmq.config 文件来调整内存阈值,默认值是 0.4,如下所示:
[{rabbit, [{vm_memory_high_watermark, 0.4}]}].
也可以用命令动态设置,如果设置成 0,则所有的消息都不能发布。
rabbitmqctl set_vm_memory_high_watermark 0.3
4.2 磁盘控制
另一种方式是通过磁盘来控制消息的发布。当磁盘空间低于指定的值时(默认50MB),触发流控措施。
例如:指定为磁盘的 30%或者 2GB:
disk_free_limit.relative = 3.0
disk_free_limit.absolute = 2GB
5. 消费端限流
默认情况下,如果不进行配置,RabbitMQ 会尽可能快速地把队列中的消息发送到消费者。因为消费者会在本地缓存消息,如果消息数量过多,可能会导致 OOM 或者影响其他进程的正常运行。
在消费者处理消息的能力有限,例如消费者数量太少,或者单条消息的处理时间过长的情况下,如果我们希望在一定数量的消息消费完之前,不再推送消息过来,就要用到消费端的流量限制措施。
可以基于 Consumer 或者 channel 设置 prefetch count 的值,含义为 Consumer端的最大的 unacked messages 数目。当超过这个数值的消息未被确认,RabbitMQ 会停止投递新的消息给该消费者。
channel.basicQos(2); // 如果超过 2 条消息没有发送 ACK,当前消费者不再接受队列消息
channel.basicConsume(QUEUE_NAME, false, consumer);
Spring Boot 配置:
spring.rabbitmq.listener.simple.prefetch=2
举例:
channel 的 prefetch count 设置为 5。当消费者有 5 条消息没有给 Broker 发送 ACK后,RabbitMQ 不再给这个消费者投递消息。