RabbitMQ死信队列,延时队列

1.前言

    之前一直听说过RabbitMQ的死信队列,延时队列,但是没有深入的了解,结合自己实际工作中使用到的RabbitMQ,现在整理出来当做个人学习笔记。

2.死信队列介绍

  死信队列:DLX(dead-letter-exchange),利用DLX,当消息在一个队列中变成死信(dead message)之后他能被重新push到另外一个Exchange,那么这个Exchange就是DLX。

  消息变成死信一般有一下三种情况:

    (1)消息被拒(basic.reject or basic.nack)并且没有重新入队(requeue=false);

    (2)当前队列中的消息数量已经超过最大长度(创建队列时指定" x-max-length参数设置队列最大消息数量)。

    (3)消息在队列中过期,即当前消息在队列中的存活时间已经超过了预先设置的TTL(Time To Live)时间;

3.死信处理过程

    (1)DLX也是一个正常的交换机(Exchange),和一般的Exchange没有区别,他能在任何的队列上指定,实际上就是设置某个队列的属性。

    (2)当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发送到设置的Exchange中,进而被路由到另一个队列。

    (3)可以监听到这个队列(DLX)做特殊处理。

4.demo演示

  设置场景1:Q1中队列数据不完整,就算从新处理也会报错,那就可以不ack,把这个消息转到死信队列另外处理。

生产者:

using RabbitMQ.Client;
using System;
using System.Collections.Generic;
using System.Text;

namespace RabbitMqClient
{
    class Program
    {
        static IConnectionFactory factory = new ConnectionFactory()
        {
            HostName = "127.0.0.1",
            UserName = "admin",
            Password = "admin",
            VirtualHost = "/"
        };
        static void Main(string[] args)
        {
            Console.WriteLine("Hello Producer!");
            SendMessage();
            Console.ReadLine();
        }

        public static void SendMessage()
        {
            //死信交换机
            string dlxexChange = "dlx.exchange";
            //死信队列
            string dlxQueueName = "dlx.queue";

            //消息交换机
            string exchange = "direct-exchange";
            //消息队列
            string queueName = "queue_a";

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

                    //创建死信交换机
                    channel.ExchangeDeclare(dlxexChange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建死信队列
                    channel.QueueDeclare(dlxQueueName, durable: true, exclusive: false, autoDelete: false);
                    //死信队列绑定死信交换机
                    channel.QueueBind(dlxQueueName, dlxexChange, routingKey: dlxQueueName);

                    // 创建消息交换机
                    channel.ExchangeDeclare(exchange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建消息队列,并指定死信队列
                    channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false, arguments:
                                        new Dictionary<string, object> {
                                             { "x-dead-letter-exchange",dlxexChange}, //设置当前队列的DLX(死信交换机)
                                             { "x-dead-letter-routing-key",dlxQueueName}, //设置DLX的路由key,DLX会根据该值去找到死信消息存放的队列
                                         });
                    //消息队列绑定消息交换机
                    channel.QueueBind(queueName, exchange, routingKey: queueName);

                    string message = "hello rabbitmq message";
                    var properties = channel.CreateBasicProperties();
                    properties.Persistent = true;
                    //发布消息
                    channel.BasicPublish(exchange: exchange,
                                         routingKey: queueName,
                                         basicProperties: properties,
                                         body: Encoding.UTF8.GetBytes(message));
                    Console.WriteLine($"向队列:{queueName}发送消息:{message}");
                }
            }
        }
    }
}

其中在队列queue_a上设置属性值x-dead-letter-exchangex-dead-letter-routing-key

消费者:

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Collections.Generic;
using System.Text;

namespace RabbitMqServer
{
    class Program
    {
        static IConnectionFactory factory = new ConnectionFactory()
        {
            HostName = "127.0.0.1",
            UserName = "admin",
            Password = "admin",
            VirtualHost = "/"
        };
        static void Main(string[] args)
        {
            Console.WriteLine("Hello Consumer!");
            Consumer();
            Console.ReadLine();
        }
        public static void Consumer()
        {
            //死信交换机
            string dlxexChange = "dlx.exchange";
            //死信队列
            string dlxQueueName = "dlx.queue";

            //消息交换机
            string exchange = "direct-exchange";
            //消息队列
            string queueName = "queue_a";
            var connection = factory.CreateConnection();
            {
                //创建信道
                var channel = connection.CreateModel();
                {

                    //创建死信交换机
                    channel.ExchangeDeclare(dlxexChange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建死信队列
                    channel.QueueDeclare(dlxQueueName, durable: true, exclusive: false, autoDelete: false);
                    //死信队列绑定死信交换机
                    channel.QueueBind(dlxQueueName, dlxexChange, routingKey: dlxQueueName);

                    // 创建消息交换机
                    channel.ExchangeDeclare(exchange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建消息队列,并指定死信队列
                    channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false, arguments:
                                        new Dictionary<string, object> {
                                             { "x-dead-letter-exchange",dlxexChange}, //设置当前队列的DLX
                                             { "x-dead-letter-routing-key",dlxQueueName}, //设置DLX的路由key,DLX会根据该值去找到死信消息存放的队列
                                         });
                    //消息队列绑定消息交换机
                    channel.QueueBind(queueName, exchange, routingKey: queueName);

                    var consumer = new EventingBasicConsumer(channel);
                    channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: true);
                    consumer.Received += (model, ea) =>
                    {
                        //处理业务
                        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                        Console.WriteLine($"队列{queueName}消费消息:{message},不做ack确认");
                        //channel.BasicAck(ea.DeliveryTag, false);
                        //不ack(BasicNack),且不把消息放回队列(requeue:false)
                        channel.BasicNack(ea.DeliveryTag, false, requeue: false);
                    };
                    channel.BasicConsume(queueName, autoAck: false, consumer);
                }
            }
        }
    }
}

消费者加上channel.BasickNack()模拟消息处理不了,不ack确认。

 

 然后我们到RabbitMQ的web端看下

 

 看到消息队列为queue_a,特性有DLX(死信交换机),DLK(死信路由)。因为消费端不nack,触发了死信,被转发到了死信队列dlx.queue。

 

5.延时队列

延时队列是配合死信队列一起使用,给队列添加消息过时时间(TTL)变成延时队列。

简单的描述就是:P(生产者)发送消息到Q1(延时队列),Q1的消息有过期时间,比如10s,那10s后消息过期就会触发死信,从而把消息转发到Q2(死信队列)。

解决问题场景:像商城下单,未支付时取消订单场景。下单时写一条记录入Q1,延时30分钟后转到Q2,消费Q2,检查订单,支付则不做操作,没支付则取消订单,恢复库存。

生产者:

public static void SendMessage_delay()
        {
            //死信交换机
            string dlxexChange = "dlx.exchange";
            //死信队列
            string dlxQueueName = "dlx.queue";

            //消息交换机
            string exchange = "direct-exchange";
            //消息队列
            string queueName = "queue_a";

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

                    //创建死信交换机
                    channel.ExchangeDeclare(dlxexChange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建死信队列
                    channel.QueueDeclare(dlxQueueName, durable: true, exclusive: false, autoDelete: false);
                    //死信队列绑定死信交换机
                    channel.QueueBind(dlxQueueName, dlxexChange, routingKey: dlxQueueName);

                    // 创建消息交换机
                    channel.ExchangeDeclare(exchange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建消息队列,并指定死信队列
                    channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false, arguments:
                                        new Dictionary<string, object> {
                                             { "x-dead-letter-exchange",dlxexChange}, //设置当前队列的DLX(死信交换机)
                                             { "x-dead-letter-routing-key",dlxQueueName}, //设置DLX的路由key,DLX会根据该值去找到死信消息存放的队列
                                            { "x-message-ttl",10000}
                                        });
                    //消息队列绑定消息交换机
                    channel.QueueBind(queueName, exchange, routingKey: queueName);

                    string message = "hello rabbitmq message";
                    var properties = channel.CreateBasicProperties();
                    properties.Persistent = true;
                    //发布消息
                    for (int i = 0; i < 10; i++)
                    {
                        message += i;
                        channel.BasicPublish(exchange: exchange,
                                         routingKey: queueName,
                                         basicProperties: properties,
                                         body: Encoding.UTF8.GetBytes(message));
                        Console.WriteLine($"{DateTime.Now}向队列:{queueName}发送消息:{message}");
                        System.Threading.Thread.Sleep(1000);
                    }

                }
            }
        }

消费者:

public static void Consumer_delay()
        {
            //死信交换机
            string dlxexChange = "dlx.exchange";
            //死信队列
            string dlxQueueName = "dlx.queue";

            var connection = factory.CreateConnection();
            {
                //创建信道
                var channel = connection.CreateModel();
                {

                    //创建死信交换机
                    channel.ExchangeDeclare(dlxexChange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建死信队列
                    channel.QueueDeclare(dlxQueueName, durable: true, exclusive: false, autoDelete: false);
                    //死信队列绑定死信交换机
                    channel.QueueBind(dlxQueueName, dlxexChange, routingKey: dlxQueueName);

                    var consumer = new EventingBasicConsumer(channel);
                    channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: true);
                    consumer.Received += (model, ea) =>
                    {
                        //处理业务
                        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                        Console.WriteLine($"{DateTime.Now}队列{dlxQueueName}消费消息:{message}");
                        channel.BasicAck(ea.DeliveryTag, false);
                        //不ack(BasicNack),且不把消息放回队列(requeue:false)
                        //channel.BasicNack(ea.DeliveryTag, false, requeue: false);
                    };
                    channel.BasicConsume(dlxQueueName, autoAck: false, consumer);
                }
            }
        }

 

 向延时队列发送消息,监听死信队列,发送和收到消息时间刚好是设置的10s。

6.延时消息设置不同过期时间

上面的延时队列能解决消息过期时间都是相同的场景,能不能解决消息的过期时间是不一样的呢?

例如场景:机器人客服,为了更像人为操作,收到消息后要随机3-10秒回复客户。

1)队列不设置TTL(消息过期时间),把过期时间设置在消息上。

生产者:

public static void SendMessage_delay()
        {
            //死信交换机
            string dlxexChange = "dlx.exchange";
            //死信队列
            string dlxQueueName = "dlx.queue";

            //消息交换机
            string exchange = "direct-exchange";
            //消息队列
            string queueName = "queue_a";

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

                    //创建死信交换机
                    channel.ExchangeDeclare(dlxexChange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建死信队列
                    channel.QueueDeclare(dlxQueueName, durable: true, exclusive: false, autoDelete: false);
                    //死信队列绑定死信交换机
                    channel.QueueBind(dlxQueueName, dlxexChange, routingKey: dlxQueueName);

                    // 创建消息交换机
                    channel.ExchangeDeclare(exchange, type: ExchangeType.Direct, durable: true, autoDelete: false);
                    //创建消息队列,并指定死信队列
                    channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false, arguments:
                                        new Dictionary<string, object> {
                                             { "x-dead-letter-exchange",dlxexChange}, //设置当前队列的DLX(死信交换机)
                                             { "x-dead-letter-routing-key",dlxQueueName}, //设置DLX的路由key,DLX会根据该值去找到死信消息存放的队列
                                        });
                    //消息队列绑定消息交换机
                    channel.QueueBind(queueName, exchange, routingKey: queueName);

                    string message = "hello rabbitmq message 10s后处理";
                    var properties = channel.CreateBasicProperties();
                    properties.Persistent = true;
                    properties.Expiration = "10000";
                    //发布消息
                    channel.BasicPublish(exchange: exchange,
                                     routingKey: queueName,
                                     basicProperties: properties,
                                     body: Encoding.UTF8.GetBytes(message));
                    Console.WriteLine($"{DateTime.Now}向队列:{queueName}发送消息:{message}");


                    message = "hello rabbitmq message 5s后处理";
                    properties = channel.CreateBasicProperties();
                    properties.Persistent = true;
                    properties.Expiration = "5000";
                    channel.BasicPublish(exchange: exchange,
                                    routingKey: queueName,
                                    basicProperties: properties,
                                    body: Encoding.UTF8.GetBytes(message));
                    Console.WriteLine($"{DateTime.Now}向队列:{queueName}发送消息:{message}");

                }
            }
        }

 消费者代码和上面一致。

结论:

生产者向队列中发送一条延时10s的消息再发一条延时5秒的消息,但消费者却先拿到延时10s的,再拿到延时5秒的,我想要的结果是先拿到延时5s的再拿到延时10s的,是什么原因呢?

原因是:队列是先进先出的,而RabbitMQ只会对首位第一条消息做检测,第一条没过期,那么后面的消息就会阻塞住等待前面的过期。

解决办法:增加一个消费者对延时队列消费,不ack,把第一条消息放到队列尾部。一直让消息在流动,这样就能检测到了。

新增消费者代码:

  

public static void Consumer_NormalConsumer()
        {
            //死信队列
            string dlxQueueName = "queue_a";

            var connection = factory.CreateConnection();
            {
                //创建信道
                var channel = connection.CreateModel();
                {
                    var consumer = new EventingBasicConsumer(channel);
                    channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: true);
                    consumer.Received += (model, ea) =>
                    {
                        //处理业务
                        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                        //Console.WriteLine($"{DateTime.Now}队列{dlxQueueName}消费消息:{message}");
                        //不ack(BasicNack),且不把消息放回队列(requeue:false)
                        System.Threading.Thread.Sleep(20);
                        channel.BasicNack(ea.DeliveryTag, false, requeue: true);
                    };
                    channel.BasicConsume(dlxQueueName, autoAck: false, consumer);
                }
            }
        }

 还有一种方式是通过安装rabbitmq_delayed_message_exchange插件的形式

 

 生产者代码:

public static void SendMessage_delay_byplugin()
        {
            //延时交换机
            string delayExchange = "dlx.exchange";
            //延时队列
            string delayQueueName = "dlx.queue";
            using (var connection = factory.CreateConnection())
            {
                using (var channel = connection.CreateModel())
                {

                    //创建延时交换机
                    channel.ExchangeDeclare(delayExchange, type: "x-delayed-message", durable: true, autoDelete: false, new Dictionary<string, object> {
                        { "x-delayed-type","direct"}
                    });
                    //创建死信队列
                    channel.QueueDeclare(delayQueueName, durable: true, exclusive: false, autoDelete: false);
                    //死信队列绑定死信交换机
                    channel.QueueBind(delayQueueName, delayExchange, routingKey: delayQueueName);


                    string message = "hello rabbitmq message 10s后处理";
                    var properties = channel.CreateBasicProperties();
                    properties.Persistent = true;
                    properties.Headers = new Dictionary<string, object> { { "x-delay", "10000" } };
                    //发布消息
                    channel.BasicPublish(exchange: delayExchange,
                                     routingKey: delayQueueName,
                                     basicProperties: properties,
                                     body: Encoding.UTF8.GetBytes(message));
                    Console.WriteLine($"{DateTime.Now}向队列:{delayQueueName}发送消息:{message}");


                    message = "hello rabbitmq message 5s后处理";
                    properties = channel.CreateBasicProperties();
                    properties.Persistent = true;
                    properties.Headers = new Dictionary<string, object> { { "x-delay", "5000" } };
                    channel.BasicPublish(exchange: delayExchange,
                                    routingKey: delayQueueName,
                                    basicProperties: properties,
                                    body: Encoding.UTF8.GetBytes(message));
                    Console.WriteLine($"{DateTime.Now}向队列:{delayQueueName}发送消息:{message}");

                }
            }
        }

消费者监听当前的队列即可:

public static void Consumer_delay_plugin()
        {
            //死信队列
            string dlxQueueName = "dlx.queue";

            var connection = factory.CreateConnection();
            {
                //创建信道
                var channel = connection.CreateModel();
                {
                    var consumer = new EventingBasicConsumer(channel);
                    channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: true);
                    consumer.Received += (model, ea) =>
                    {
                        //处理业务
                        var message = Encoding.UTF8.GetString(ea.Body.ToArray());
                        Console.WriteLine($"{DateTime.Now}队列{dlxQueueName}消费消息:{message}");
                        //不ack(BasicNack),且不把消息放回队列(requeue:false)
                        System.Threading.Thread.Sleep(20);
                        channel.BasicAck(ea.DeliveryTag, false);
                        //channel.BasicNack(ea.DeliveryTag, false, requeue: true);
                    };
                    channel.BasicConsume(dlxQueueName, autoAck: false, consumer);
                }
            }
        }

7.demo地址

https://github.com/wg296613796/RabbitMQDemo.git

posted @ 2021-09-01 11:46  可乐加冰-Mr-Wang  阅读(304)  评论(0)    收藏  举报