消费端限流

1. 为什么要对消费端限流

假设一个场景,首先,我们 Rabbitmq 服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这样情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!

当数据量特别大的时候,我们对生产端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束生产端,这是用户的行为。所以我们应该对消费端限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。

2.限流的 api 讲解

RabbitMQ 提供了一种 qos (服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于 consume 或者 channel 设置 Qos 的值)未被确认前,不进行消费新的消息。

  1. /**
  2. * Request specific "quality of service" settings.
  3. * These settings impose limits on the amount of data the server
  4. * will deliver to consumers before requiring acknowledgements.
  5. * Thus they provide a means of consumer-initiated flow control.
  6. * @param prefetchSize maximum amount of content (measured in
  7. * octets) that the server will deliver, 0 if unlimited
  8. * @param prefetchCount maximum number of messages that the server
  9. * will deliver, 0 if unlimited
  10. * @param global true if the settings should be applied to the
  11. * entire channel rather than each consumer
  12. * @throws java.io.IOException if an error is encountered
  13. */
  14. void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
  • prefetchSize:0,单条消息大小限制,0代表不限制

  • prefetchCount:一次性消费的消息数量。会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,即一旦有 N 个消息还没有 ack,则该 consumer 将 block 掉,直到有消息 ack。

  • global:true、false 是否将上面设置应用于 channel,简单点说,就是上面限制是 channel 级别的还是 consumer 级别。当我们设置为 false 的时候生效,设置为 true 的时候没有了限流功能,因为 channel 级别尚未实现。

  • 注意:prefetchSize 和 global 这两项,rabbitmq 没有实现,暂且不研究。特别注意一点,prefetchCount 在 no_ask=false 的情况下才生效,即在自动应答的情况下这两个值是不生效的。

3.如何对消费端进行限流

  • 首先第一步,我们既然要使用消费端限流,我们需要关闭自动 ack,将 autoAck 设置为 falsechannel.basicConsume(queueName, false, consumer);

  • 第二步我们来设置具体的限流大小以及数量。channel.basicQos(0, 15, false);

  • 第三步在消费者的 handleDelivery 消费方法中手动 ack,并且设置批量处理 ack 回应为 truechannel.basicAck(envelope.getDeliveryTag(), true);

这是生产端代码,与前几章的生产端代码没有做任何改变,主要的操作集中在消费端。

  1. import com.rabbitmq.client.Channel;
  2. import com.rabbitmq.client.Connection;
  3. import com.rabbitmq.client.ConnectionFactory;
  4. public class QosProducer {
  5. public static void main(String[] args) throws Exception {
  6. //1. 创建一个 ConnectionFactory 并进行设置
  7. ConnectionFactory factory = new ConnectionFactory();
  8. factory.setHost("localhost");
  9. factory.setVirtualHost("/");
  10. factory.setUsername("guest");
  11. factory.setPassword("guest");
  12. //2. 通过连接工厂来创建连接
  13. Connection connection = factory.newConnection();
  14. //3. 通过 Connection 来创建 Channel
  15. Channel channel = connection.createChannel();
  16. //4. 声明
  17. String exchangeName = "test_qos_exchange";
  18. String routingKey = "item.add";
  19. //5. 发送
  20. String msg = "this is qos msg";
  21. for (int i = 0; i < 10; i++) {
  22. String tem = msg + " : " + i;
  23. channel.basicPublish(exchangeName, routingKey, null, tem.getBytes());
  24. System.out.println("Send message : " + tem);
  25. }
  26. //6. 关闭连接
  27. channel.close();
  28. connection.close();
  29. }
  30. }

这里我们创建一个消费者,通过以下代码来验证限流效果以及 global 参数设置为 true 时不起作用.。我们通过Thread.sleep(5000); 来让 ack 即处理消息的过程慢一些,这样我们就可以从后台管理工具中清晰观察到限流情况。

  1. import com.rabbitmq.client.*;
  2. import java.io.IOException;
  3. public class QosConsumer {
  4. public static void main(String[] args) throws Exception {
  5. //1. 创建一个 ConnectionFactory 并进行设置
  6. ConnectionFactory factory = new ConnectionFactory();
  7. factory.setHost("localhost");
  8. factory.setVirtualHost("/");
  9. factory.setUsername("guest");
  10. factory.setPassword("guest");
  11. factory.setAutomaticRecoveryEnabled(true);
  12. factory.setNetworkRecoveryInterval(3000);
  13. //2. 通过连接工厂来创建连接
  14. Connection connection = factory.newConnection();
  15. //3. 通过 Connection 来创建 Channel
  16. final Channel channel = connection.createChannel();
  17. //4. 声明
  18. String exchangeName = "test_qos_exchange";
  19. String queueName = "test_qos_queue";
  20. String routingKey = "item.#";
  21. channel.exchangeDeclare(exchangeName, "topic", true, false, null);
  22. channel.queueDeclare(queueName, true, false, false, null);
  23. channel.basicQos(0, 3, false);
  24. //一般不用代码绑定,在管理界面手动绑定
  25. channel.queueBind(queueName, exchangeName, routingKey);
  26. //5. 创建消费者并接收消息
  27. Consumer consumer = new DefaultConsumer(channel) {
  28. @Override
  29. public void handleDelivery(String consumerTag, Envelope envelope,
  30. AMQP.BasicProperties properties, byte[] body)
  31. throws IOException {
  32. try {
  33. Thread.sleep(5000);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. String message = new String(body, "UTF-8");
  38. System.out.println("[x] Received '" + message + "'");
  39. channel.basicAck(envelope.getDeliveryTag(), true);
  40. }
  41. };
  42. //6. 设置 Channel 消费者绑定队列
  43. channel.basicConsume(queueName, false, consumer);
  44. channel.basicConsume(queueName, false, consumer1);
  45. }
  46. }

我们从下图中发现 Unacked值一直都是 3 ,每过 5 秒 消费一条消息即 Ready 和 Total 都减少 3,而 Unacked的值在这里代表消费者正在处理的消息,通过我们的实验发现了消费者一次性最多处理 3 条消息,达到了消费者限流的预期功能。

当我们将void basicQos(int prefetchSize, int prefetchCount, boolean global)中的 global 设置为 true的时候我们发现并没有了限流的作用。

TTL

TTL是Time To Live的缩写,也就是生存时间。RabbitMQ支持消息的过期时间,在消息发送时可以进行指定。

RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会自动的清除。

这与 Redis 中的过期时间概念类似。我们应该合理使用 TTL 技术,可以有效的处理过期垃圾消息,从而降低服务器的负载,最大化的发挥服务器的性能。

RabbitMQ allows you to set TTL (time to live) for both messages and queues. This can be done using optional queue arguments or policies (the latter option is recommended). Message TTL can be enforced for a single queue, a group of queues or applied for individual messages.

RabbitMQ允许您为消息和队列设置TTL(生存时间)。 这可以使用可选的队列参数或策略来完成(建议使用后一个选项)。 可以对单个队列,一组队列强制执行消息TTL,也可以为单个消息应用消息TTL。

​ ——摘自 RabbitMQ 官方文档

1.消息的 TTL

我们在生产端发送消息的时候可以在 properties 中指定 expiration属性来对消息过期时间进行设置,单位为毫秒(ms)。

  1. /**
  2. * deliverMode 设置为 2 的时候代表持久化消息
  3. * expiration 意思是设置消息的有效期,超过10秒没有被消费者接收后会被自动删除
  4. * headers 自定义的一些属性
  5. * */
  6. //5. 发送
  7. Map<String, Object> headers = new HashMap<String, Object>();
  8. headers.put("myhead1", "111");
  9. headers.put("myhead2", "222");
  10. AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
  11. .deliveryMode(2)
  12. .contentEncoding("UTF-8")
  13. .expiration("100000")
  14. .headers(headers)
  15. .build();
  16. String msg = "test message";
  17. channel.basicPublish("", queueName, properties, msg.getBytes());

我们也可以后台管理页面中进入 Exchange 发送消息指定expiration

2.队列的 TTL

我们也可以在后台管理界面中新增一个 queue,创建时可以设置 ttl,对于队列中超过该时间的消息将会被移除。

死信队列

死信队列:没有被及时消费的消息存放的队列

消息没有被及时消费的原因:

  • a.消息被拒绝(basic.reject/ basic.nack)并且不再重新投递 requeue=false

  • b.TTL(time-to-live) 消息超时未消费

  • c.达到最大队列长度

实现死信队列步骤

  • 首先需要设置死信队列的 exchange 和 queue,然后进行绑定:

    1. Exchange: dlx.exchange
    2. Queue: dlx.queue
    3. RoutingKey: # 代表接收所有路由 key
  • 然后我们进行正常声明交换机、队列、绑定,只不过我们需要在普通队列加上一个参数即可: arguments.put("x-dead-letter-exchange",' dlx.exchange' )

  • 这样消息在过期、requeue失败、 队列在达到最大长度时,消息就可以直接路由到死信队列!

  1. import com.rabbitmq.client.AMQP;
  2. import com.rabbitmq.client.Channel;
  3. import com.rabbitmq.client.Connection;
  4. import com.rabbitmq.client.ConnectionFactory;
  5. public class DlxProducer {
  6. public static void main(String[] args) throws Exception {
  7. //设置连接以及创建 channel 湖绿
  8. String exchangeName = "test_dlx_exchange";
  9. String routingKey = "item.update";
  10. String msg = "this is dlx msg";
  11. //我们设置消息过期时间,10秒后再消费 让消息进入死信队列
  12. AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
  13. .deliveryMode(2)
  14. .expiration("10000")
  15. .build();
  16. channel.basicPublish(exchangeName, routingKey, true, properties, msg.getBytes());
  17. System.out.println("Send message : " + msg);
  18. channel.close();
  19. connection.close();
  20. }
  21. }
  1. import com.rabbitmq.client.*;
  2. import java.io.IOException;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. public class DlxConsumer {
  6. public static void main(String[] args) throws Exception {
  7. //创建连接、创建channel忽略 内容可以在上面代码中获取
  8. String exchangeName = "test_dlx_exchange";
  9. String queueName = "test_dlx_queue";
  10. String routingKey = "item.#";
  11. //必须设置参数到 arguments 中
  12. Map<String, Object> arguments = new HashMap<String, Object>();
  13. arguments.put("x-dead-letter-exchange", "dlx.exchange");
  14. channel.exchangeDeclare(exchangeName, "topic", true, false, null);
  15. //将 arguments 放入队列的声明中
  16. channel.queueDeclare(queueName, true, false, false, arguments);
  17. //一般不用代码绑定,在管理界面手动绑定
  18. channel.queueBind(queueName, exchangeName, routingKey);
  19. //声明死信队列
  20. channel.exchangeDeclare("dlx.exchange", "topic", true, false, null);
  21. channel.queueDeclare("dlx.queue", true, false, false, null);
  22. //路由键为 # 代表可以路由到所有消息
  23. channel.queueBind("dlx.queue", "dlx.exchange", "#");
  24. Consumer consumer = new DefaultConsumer(channel) {
  25. @Override
  26. public void handleDelivery(String consumerTag, Envelope envelope,
  27. AMQP.BasicProperties properties, byte[] body)
  28. throws IOException {
  29. String message = new String(body, "UTF-8");
  30. System.out.println(" [x] Received '" + message + "'");
  31. }
  32. };
  33. //6. 设置 Channel 消费者绑定队列
  34. channel.basicConsume(queueName, true, consumer);
  35. }
  36. }

总结

DLX也是一个正常的 Exchange,和一般的 Exchange 没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列中有死信时,RabbitMQ 就会自动的将这个消息重新发布到设置的 Exchange 上去,进而被路由到另一个队列。可以监听这个队列中消息做相应的处理。

RabbitMQ 消费端限流、TTL、死信队列的更多相关文章

  1. RabbitMQ消费端限流策略(十)

    消费端限流: 什么是消费端限流? 场景: 我们RabbitMQ服务器有上万条未处理的消息,我们随便打开一个消费者客户端,会出现下面情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么 ...

  2. Rabbitmq——实现消费端限流 --NACK重回队列

    如果是高并发下,rabbitmq服务器上收到成千上万条消息,那么当打开消费端时,这些消息必定喷涌而来,导致消费端消费不过来甚至挂掉都有可能. 在非自动确认的模式下,可以采用限流模式,rabbitmq ...

  3. Rabbitmq之高级特性——实现消费端限流&NACK重回队列

    如果是高并发下,rabbitmq服务器上收到成千上万条消息,那么当打开消费端时,这些消息必定喷涌而来,导致消费端消费不过来甚至挂掉都有可能. 在非自动确认的模式下,可以采用限流模式,rabbitmq ...

  4. 面试官:RabbitMQ怎么实现消费端限流

    哈喽!大家好,我是小奇,一位不靠谱的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 RabbitMQ有很多高级特性, ...

  5. SpringBoot 整合 RabbitMQ(包含三种消息确认机制以及消费端限流)

    目录 说明 生产端 消费端 说明 本文 SpringBoot 与 RabbitMQ 进行整合的时候,包含了三种消息的确认模式,如果查询详细的确认模式设置,请阅读:RabbitMQ的三种消息确认模式 同 ...

  6. RabbitMQ高级之消息限流与延时队列

    人生终将是场单人旅途,孤独之前是迷茫,孤独过后是成长. 楔子 本篇是消息队列RabbitMQ的第五弹. 上篇本来打算讲述RabbitMQ的一些高级用法: 如何保证消息的可靠性? 消息队列如何进行限流? ...

  7. RabbitMQ消费端ACK与重回队列机制,TTL,死信队列详解(十一)

    消费端的手工ACK和NACK 消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿. 如果由于服务器宕机等严重问题,那么我们就需要手工进行ACK保障消费端成功. 消费端重回队列 ...

  8. RabbitMQ消费端自定义监听(九)

    场景: 我们一般在代码中编写while循环,进行consumer.nextDelivery方法进行获取下一条消息,然后进行消费处理. 实际环境: 我们使用自定义的Consumer更加的方便,解耦性更强 ...

  9. rabbitmq消费端加入精确控频。

    控制频率之前用的是线程池的数量来控制,很难控制.因为做一键事情,做一万次,并不是每次消耗的时间都相同,所以很难推测出到底多少线程并发才刚好不超过指定的频率. 现在在框架中加入控频功能,即使开200线程 ...

随机推荐

  1. HDU 5396 区间DP 数学 Expression

    题意:有n个数字,n-1个运算符,每个运算符的顺序可以任意,因此一共有 (n - 1)! 种运算顺序,得到 (n - 1)! 个运算结果,然后求这些运算结果之和 MOD 1e9+7. 分析: 类比最优 ...

  2. 网络编程基础socket 重要中:TCP/UDP/七层协议

    计算机网络的发展及基础网络概念 问题:网络到底是什么?计算机之间是如何通信的? 早期 : 联机 以太网 : 局域网与交换机 广播 主机之间“一对所有”的通讯模式,网络对其中每一台主机发出的信号都进行无 ...

  3. python - log日志

    # -*- coding:utf-8 -*- ''' @project: jiaxy @author: Jimmy @file: study_logging.py @ide: PyCharm Comm ...

  4. sql server 学习分享

    http://www.cnblogs.com/liu-chao-feng/p/6144872.html

  5. 设计模式之工厂模式 Factory实现

    simpleFactory //car接口 public interface Car { void run(); } //两个实现类 public class Audi implements Car{ ...

  6. POJ 1656 Counting Black

    Counting Black Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 9772   Accepted: 6307 De ...

  7. 平面凸包Graham算法

    板题hdu1348Wall 平面凸包问题是计算几何中的一个经典问题 具体就是给出平面上的多个点,求一个最小的凸多边形,使得其包含所有的点 具体形象就类似平面上有若干柱子,一个人用绳子从外围将其紧紧缠绕 ...

  8. APIO2018 题解

    坑了好久,补一补. 话说我当时去参加 $APIO2018$ 了,不过纯粹打铁…… 我的程序交道人家毛子的网站上, $c++14$ 编译器不停地给我编 $RE$,只记得好像是结构体排序的问题(删掉那个排 ...

  9. bzoj 4295 [PA2015]Hazard 贪心,暴力

    [PA2015]Hazard Time Limit: 10 Sec  Memory Limit: 512 MBSubmit: 69  Solved: 19[Submit][Status][Discus ...

  10. 浅谈Linux下的五种I/O模型 两篇别人的博客

     http://blog.csdn.net/sinat_34990639/article/details/52778562  http://www.cnblogs.com/chy2055/p/5220 ...