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-exchange,x-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地址
本文来自博客园,作者:可乐加冰-Mr-Wang,转载请注明原文链接:https://www.cnblogs.com/helloworld-wang/p/15213854.html

浙公网安备 33010602011771号