第七节:RabbitMq延迟队列两种实操方案(死信交换机+TTL、延迟插件)

一. 前言

1. 什么是延迟队列?

  延迟队列是一种特殊的队列,它允许将消息或任务延迟一段时间后再进行处理1。这种队列为那些需要延迟处理的场景提供了一种可靠的延迟处理机制。其工作原理是将消息或任务存储在队列中,并为每个消息或任务设置一个延迟时间。当延迟时间到达时,队列会将消息或任务取出并进行相应的处理

2. 使用场景

  延迟队列在多种场景中都有应用,例如电商平台的订单处理。如果用户在一定时间内未支付订单,系统可以自动取消订单并退还库存。

       此外,延迟队列还可以用于会议预定系统的通知发送、安全工单的超时提醒、场景:超时订单、限时优惠、定时发布。

 

二. 基于死信交换机+ttl实现延迟队列

1. 目标

  生产者发送一个消息到队列,想让消费者延迟 n 秒后,再消费。

2. 实现原理

  声明延迟队列 delayed_queue, 绑定死信交换机dead_exchange 和 死信队列dead_queue, 延迟队列中的消息到了ttl后,会通过死信交换机dead_exchange 进入 死信队列dead_queue,  消费者最终是从 死信队列dead_queue 中消费消息。

3. 缺点

 A. 仅仅支持在队列层次上实现ttl,不支持每条消息上实现不同的ttl,如果想在消息层次上实现ttl,需要安装下面的插件rabbitmq_delayed_message_exchange【实测装了也不好用】

 B. 需要两套队列 和 交换机。

4. 优点

 不需要按照任何插件

5. 代码实操

生产者

查看代码
Console.ForegroundColor = ConsoleColor.Green;
var factory = new ConnectionFactory
{
    HostName = "47.101.xxx.xxx",
    UserName = "admin",//用户名
    Password = "xxxx"//密码 
};
 //延迟交换机、延迟队列
 const string DelayedExchangeName = "delayed_exchange";
 const string DelayedQueueName = "delayed_queue";
 //死信交换机、死信队列
 const string DeadExchangeName = "dead_exchange";
 const string DeadQueueName = "dead_queue";
 //公用的路由key,延迟交换机 和 死信交换机都必须用这个统一 的key
 const string RoutingKey = "common_key";


 using var connection = factory.CreateConnection();
 using var channel = connection.CreateModel();


 //1. 声明延迟交换机
 channel.ExchangeDeclare(DelayedExchangeName, ExchangeType.Direct);
 //声明延迟队列, 并绑定死信交换机、并设置队列中消息的tts
 channel.QueueDeclare(DelayedQueueName, false, false, false, arguments: new Dictionary<string, object>
         {
             { "x-dead-letter-exchange", DeadExchangeName }, // 绑定死信交换机
             { "x-message-ttl", 10000 } // 设置消息的TTL(10s)
         });
 channel.QueueBind(DelayedQueueName, DelayedExchangeName, RoutingKey);



 //2. 声明死信交换机和死信队列,并将二者进行绑定
 channel.ExchangeDeclare(DeadExchangeName, ExchangeType.Direct);
 channel.QueueDeclare(DeadQueueName, false, false, false, null);
 channel.QueueBind(DeadQueueName, DeadExchangeName, RoutingKey);


 //3. 发布带TTL的消息到延迟队列 
 //如果消息中不存在ttl,则使用队列默认的ttl, 如果同时存在,以少的为主
 string message = "";
 //在控制台输入消息,按enter键发送消息              
 while (!message.Equals("stop", StringComparison.CurrentCultureIgnoreCase))
 {
     Console.WriteLine("请输入要发送的消息:");
     message = Console.ReadLine();
     var body = Encoding.UTF8.GetBytes(message);
     try
     {
         //开启消息确认模式
         channel.ConfirmSelect();
         //发送消息
         var properties = channel.CreateBasicProperties();
         properties.Headers = new Dictionary<string, object>
                 {
                     { "x-delay", 5000 } // 在这里也可以通过消息头部设置x-delay,需要RabbitMQ的延迟插件支持,默认不支持 (5s),实测装上3中的延迟插件也不好用
                 };
         channel.BasicPublish(DelayedExchangeName, RoutingKey, properties, body);

         if (channel.WaitForConfirms())   //单条消息确认
         {
             //表示消息发送成功(已经存入队列)
             Console.WriteLine($"【{message}】发送成功!");
         }
         else
         {
             //表示消息发送失败
             Console.WriteLine($"【{message}】发送失败!");
         }
         //channel.WaitForConfirmsOrDie();//如果所有消息发送成功 就正常执行, 如果有消息发送失败;就抛出异常;
     }
     catch (Exception)
     {
         //表示消息发送失败
         Console.WriteLine($"【{message}】发送失败!");
     }
 }

消费者

查看代码
 Console.ForegroundColor = ConsoleColor.Green;
var factory = new ConnectionFactory
{
    HostName = "47.101.xxx.xxx",
    UserName = "admin",//用户名
    Password = "xxx"//密码 
};
  const string DeadQueueName = "dead_queue";  //死信队列


  var connection = factory.CreateConnection();
  using IModel channel = connection.CreateModel();
  // 消费目标队列中的消息
  var consumer = new EventingBasicConsumer(channel);
  consumer.Received += (model, ea) =>
  {
      try
      {
          var message = Encoding.UTF8.GetString(ea.Body.ToArray());
          //输出到控制台,表示消费成功
          Console.WriteLine(message);

          //手动确认  (下面模拟消息正常消费,告诉rabbitmq,可以删除该条消息)
          channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
      }
      catch (Exception ex)
      {
          Console.WriteLine(ex.Message);   //表示消费失败

          //手动确认  (下面模拟消息异常消费,告诉rabbitmq,可以删除该条消息,也可以重新写入队列)
          // requeue: true:重新写入到队列里去; false: 删除消息
          channel.BasicReject(deliveryTag: ea.DeliveryTag, requeue: true);
      }
  };

  Console.WriteLine("------------------消费者准备就绪-------------------------");
  // 处理消息 autoAck: false  不使用自动确认,采用手动确认
  channel.BasicConsume(queue: DeadQueueName, autoAck: false, consumer: consumer);
  Console.ReadKey();

 

测试结果

   生产者发送一条消息,消费者需要10s后才能收到, 消息层次上的ttl无效。

 

三. 基于延迟插件实现【推荐-简单】

1. 目标

   生产者发送一个消息到队列,想让消费者延迟 n 秒后,再消费。

2. 原理

  使用 rabbitmq_delayed_message_exchange 插件中的延迟队列,直接就可以实现延迟功能。

  即将消息发送到延迟队列delayed_queue,消费者直接从 delayed_queue 队列中消费即可,到了ttl后,就可以消费了。

3. 优点

 A. 只有一个队列就可以实现

 B. 可以实现消息层面上设置ttl

4. 缺点

 需要安装插件

5. 插件安装步骤

 (1).下载插件 rabbitmq_delayed_message_exchange

    去下载地址,找和自己rabbitmq版本相同的插件,下载 .ez 文件,rabbitmq_delayed_message_exchange-3.8.9-0199d11c.ez

    https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases

 (2).将这个插件copy到安装目录下的plugins文件夹下

 (3).去sbin目录文件夹下,运行指令 【rabbitmq-plugins enable rabbitmq_delayed_message_exchange】,如图所示

 (4). 重启rabbitmq 【net stop rabbitmq】【net start rabbitmq】

 

 6. 代码分享

生产者

查看代码
 Console.ForegroundColor = ConsoleColor.Red;
ConnectionFactory factory = new()
{
    HostName = "47.101.xxx.xxx",
    UserName = "admin",//用户名
    Password = "xxxx"//密码 
};
const string DelayedExchangeName = "delayed_exchange2";
const string QueueName = "delayed_queue2";
const string RoutingKey = "delay_key2";

using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

//1. 声明延迟交换机
//首先声明了一个类型为 x-delayed-message 的交换机,并指定了实际的交换机类型(在这个例子中是 direct)
var arguments = new Dictionary<string, object>
        {
            { "x-delayed-type", "direct" } // 实际的交换机类型
        };
channel.ExchangeDeclare(DelayedExchangeName, "x-delayed-message", true, false, arguments: arguments);
//2. 声明队列
channel.QueueDeclare(QueueName, false, false, false, null);
//3. 绑定队列到交换机
channel.QueueBind(QueueName, DelayedExchangeName, RoutingKey);


//4. 发布带TTL的消息到延迟队列 
//如果消息中不存在ttl,则使用队列默认的ttl, 如果同时存在,以少的为主
string message = "";
//在控制台输入消息,按enter键发送消息              
while (!message.Equals("stop", StringComparison.CurrentCultureIgnoreCase))
{
    Console.WriteLine("请输入要发送的消息:");
    message = Console.ReadLine();
    var body = Encoding.UTF8.GetBytes(message);
    try
    {
        //开启消息确认模式
        channel.ConfirmSelect();
        //发送消息
        var properties = channel.CreateBasicProperties();
        properties.Headers = new Dictionary<string, object>
                {
                    { "x-delay", 5000 } // 需要rabbitmq_delayed_message_exchange的延迟插件支持,(5s)
                };
        channel.BasicPublish(DelayedExchangeName, RoutingKey, properties, body);

        if (channel.WaitForConfirms())   //单条消息确认
        {
            //表示消息发送成功(已经存入队列)
            Console.WriteLine($"【{message}】发送成功!");
        }
        else
        {
            //表示消息发送失败
            Console.WriteLine($"【{message}】发送失败!");
        }
    }
    catch (Exception)
    {
        //表示消息发送失败
        Console.WriteLine($"【{message}】发送失败!");
    }
}

消费者 

查看代码
 Console.ForegroundColor = ConsoleColor.Red;
ConnectionFactory factory = new()
{
    HostName = "47.101.xxx.xxx",
    UserName = "admin",//用户名
    Password = "xxxx"//密码 
};
  const string QueueName = "delayed_queue2";

  var connection = factory.CreateConnection();
  using IModel channel = connection.CreateModel();
  // 消费目标队列中的消息
  var consumer = new EventingBasicConsumer(channel);
  consumer.Received += (model, ea) =>
  {
      try
      {
          var message = Encoding.UTF8.GetString(ea.Body.ToArray());
          //输出到控制台,表示消费成功
          Console.WriteLine(message);

          //手动确认  (下面模拟消息正常消费,告诉rabbitmq,可以删除该条消息)
          channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
      }
      catch (Exception ex)
      {
          Console.WriteLine(ex.Message);   //表示消费失败

          //手动确认  (下面模拟消息异常消费,告诉rabbitmq,可以删除该条消息,也可以重新写入队列)
          // requeue: true:重新写入到队列里去; false: 删除消息
          channel.BasicReject(deliveryTag: ea.DeliveryTag, requeue: true);
      }
  };

  Console.WriteLine("------------------消费者准备就绪-------------------------");
  // 处理消息 autoAck: false  不使用自动确认,采用手动确认
  channel.BasicConsume(queue: QueueName, autoAck: false, consumer: consumer);
  Console.ReadKey();

测试结果

  生产者发送一条消息,消费者需要5s后才能收到, 消息层次上的ttl可以使用!!!!

 

 

 

 

 

 

 

 

 

!

作       者 : Yaopengfei(姚鹏飞)

博客地址 : http://www.cnblogs.com/yaopengfei/

声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。

声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。

 
posted @ 2024-05-30 10:07  Yaopengfei  阅读(345)  评论(1编辑  收藏  举报