RabbitMQ死信队列与延迟队列
简单研究下消息、队列的生存时间,以及死信队列、延迟队列。
简单的说:
(1) 死信队列就是消息进入另一个交换机,可以修改其routingKey进入另一个队列。发生的情况为:当程序手动basicReject(false) 、消息TTL过期、队列达到最大长度。
(2)队列和消息都有个TTL生存时间,队列的TTL到达后队列会自动删除,消息不会进入死信队列;消息的生存时间到达后会进入死信队列。消息的生存时间可以在队列设置所有消息的TTL,也可以对某个消息单独设置TTL
(3) 延迟队列就是利用死信队列,给消息设置TTL,到期后进入另一个死信队列,我们可以监听另一个死信队列。
1. 简介
死信队列:DLX,dead-letter-exchange。利用DLX,当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。DLX是一个正常的交换机,它们可以像普通的交换机一样使用。
延迟队列:利用死信可以实现延迟队列。 比如设置队列有限期为60s,到期移动到另一个队列。比如订单30s,30s之后移动到另一个死信队列,我们可以监听另一个死信队列。
1. 进入死信队列的情况:
消息被拒绝(basic.reject / basic.nack),并且requeue = false
消息TTL过期。TTL:Time To Live的简称,即过期时间。RabbitMQ可以对消息和队列设置TTL。
队列达到最大长度
2.处理过程
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。可以监听这个队列中的消息做相应的处理。
2.队列和消息的生存时间
参考:https://www.rabbitmq.com/ttl.html
首先简单研究下队列和消息的生存时间的使用。
1.给队列中的消息设置ttl为1min
Define Message TTL for Queues Using x-arguments During Declaration
package rabbitmq; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; public class Producer { public static ConnectionFactory getConnectionFactory() { // 创建连接工程,下面给出的是默认的case ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.99.100"); factory.setPort(5672); factory.setUsername("guest"); factory.setPassword("guest"); factory.setVirtualHost("/"); return factory; } public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory connectionFactory = getConnectionFactory(); Connection newConnection = null; Channel createChannel = null; try { newConnection = connectionFactory.newConnection(); createChannel = newConnection.createChannel(); /** * 声明一个队列。 * 参数一:队列名称 * 参数二:是否持久化 * 参数三:是否排外 如果排外则这个队列只允许有一个消费者 * 参数四:是否自动删除队列,如果为true表示没有消息也没有消费者连接自动删除队列 * 参数五:队列的附加属性 * 注意: * 1.声明队列时,如果已经存在则放弃声明,如果不存在则会声明一个新队列; * 2.队列名可以任意取值,但需要与消息接收者一致。 * 3.下面的代码可有可无,一定在发送消息前确认队列名称已经存在RabbitMQ中,否则消息会发送失败。 */ Map<String, Object> arg = new HashMap<String, Object>(); arg.put("x-message-ttl", 60000); createChannel.queueDeclare("myQueue", true, false, false,arg); String message = "测试消息"; /** * 发送消息到MQ * 参数一:交换机名称,为""表示不用交换机 * 参数二:为队列名称或者routingKey.当指定了交换机就是routingKey * 参数三:消息的属性信息 * 参数四:消息内容的字节数组 */ createChannel.basicPublish("", "myQueue", null, message.getBytes()); System.out.println("消息发送成功"); } catch (Exception e) { e.printStackTrace(); } finally { if (createChannel != null) { createChannel.close(); } if (newConnection != null) { newConnection.close(); } } } }
结果:可以看到队列有ttl属性,并且1 min后消息自动丢弃。
2. 设置消息的过期时间
只对单个消息有效。核心代码如下:
createChannel.queueDeclare("myQueue", true, false, false, null); String message = "测试消息"; AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() .expiration("60000") .build(); createChannel.basicPublish("", "myQueue", properties, message.getBytes());
结果1min后消息自动丢弃。
3.给队列设置生存时间(了解即可,超时之后不进入死信)
This example in Java creates a queue which expires after it has been unused for 1 minutes.
Map<String, Object> arg = new HashMap<String, Object>(); arg.put("x-expires", 60000); createChannel.queueDeclare("myQueue", true, false, false, arg); String message = "测试消息"; createChannel.basicPublish("", "myQueue", null, message.getBytes());
结果:队列有生存时间,1min后队列自动删除。
3.死信队列Dead Letter Exchanges (DLX)
参考: https://www.rabbitmq.com/dlx.html
官网中队发生死信的情况解释如下:
The message is negatively acknowledged by a consumer using basic.reject or basic.nack with requeue parameter set to false.
The message expires due to per-message TTL; or
The message is dropped because its queue exceeded a length limit
(Note that expiration of a queue will not dead letter the messages in it.)也就是队列生存时间达到之后不会进入死信队列。
1. 死信队列的简单使用
1. 生产者:
声明队列的时候用属性指定其死信队列交换机名称。
测试:
package rabbitmq; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; public class Producer { public static ConnectionFactory getConnectionFactory() { // 创建连接工程,下面给出的是默认的case ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.99.100"); factory.setPort(5672); factory.setUsername("guest"); factory.setPassword("guest"); factory.setVirtualHost("/"); return factory; } public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory connectionFactory = getConnectionFactory(); Connection newConnection = null; Channel createChannel = null; try { newConnection = connectionFactory.newConnection(); createChannel = newConnection.createChannel(); // 声明一个正常的direct类型的交换机 createChannel.exchangeDeclare("order.exchange", BuiltinExchangeType.DIRECT); // 声明死信交换机为===order.dead.exchange String dlxName = "order.dead.exchange"; createChannel.exchangeDeclare(dlxName, BuiltinExchangeType.DIRECT); // 声明队列并指定死信交换机为上面死信交换机 Map<String, Object> arg = new HashMap<String, Object>(); arg.put("x-dead-letter-exchange", dlxName); createChannel.queueDeclare("myQueue", true, false, false, arg); String message = "测试消息"; createChannel.basicPublish("order.exchange", "routing_key_myQueue", null, message.getBytes()); System.out.println("消息发送成功"); } catch (Exception e) { e.printStackTrace(); } finally { if (createChannel != null) { createChannel.close(); } if (newConnection != null) { newConnection.close(); } } } }
结果:
(1)生成两个Exchange
(2)队列myQueue的死信队列有属性
2. 消费者:
一个消费者监听正常队列,一个消费者监听死信队列。(只是绑定的交换机不同)
消费者一:监听正常队列
package rabbitmq; import java.io.IOException; import java.util.Date; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.AMQP.BasicProperties; public class Consumer { public static ConnectionFactory getConnectionFactory() { // 创建连接工程,下面给出的是默认的case ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.99.100"); factory.setPort(5672); factory.setUsername("guest"); factory.setPassword("guest"); factory.setVirtualHost("/"); return factory; } public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory connectionFactory = getConnectionFactory(); Connection newConnection = null; Channel createChannel = null; try { newConnection = connectionFactory.newConnection(); createChannel = newConnection.createChannel(); // 队列绑定交换机-channel.queueBind(队列名, 交换机名, 路由key[广播消息设置为空串]) createChannel.queueBind("myQueue", "order.exchange", "routing_key_myQueue"); createChannel.basicConsume("myQueue", false, "", new DefaultConsumer(createChannel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { System.out.println("consumerTag: " + consumerTag); System.out.println("envelope: " + envelope); System.out.println("properties: " + properties); String string = new String(body, "UTF-8"); System.out.println("接收到消息: -》 " + string); long deliveryTag = envelope.getDeliveryTag(); Channel channel = this.getChannel(); System.out.println("拒绝消息, 使之进入死信队列"); System.out.println("时间: " + new Date()); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { } // basicReject第二个参数为false进入死信队列或丢弃 channel.basicReject(deliveryTag, false); } }); } catch (Exception e) { e.printStackTrace(); } finally { } } }
消费者二:监听死信队列
package rabbitmq; import java.io.IOException; import java.util.Date; import java.util.concurrent.TimeoutException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.AMQP.BasicProperties; public class Consumer2 { public static ConnectionFactory getConnectionFactory() { // 创建连接工程,下面给出的是默认的case ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.99.100"); factory.setPort(5672); factory.setUsername("guest"); factory.setPassword("guest"); factory.setVirtualHost("/"); return factory; } public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory connectionFactory = getConnectionFactory(); Connection newConnection = null; Channel createChannel = null; try { newConnection = connectionFactory.newConnection(); createChannel = newConnection.createChannel(); // 队列绑定交换机-channel.queueBind(队列名, 交换机名, 路由key[广播消息设置为空串]) createChannel.queueBind("myQueue", "order.dead.exchange", "routing_key_myQueue"); createChannel.basicConsume("myQueue", false, "", new DefaultConsumer(createChannel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { System.out.println("时间: " + new Date()); System.out.println("consumerTag: " + consumerTag); System.out.println("envelope: " + envelope); System.out.println("properties: " + properties); String string = new String(body, "UTF-8"); System.out.println("接收到消息: -》 " + string); long deliveryTag = envelope.getDeliveryTag(); Channel channel = this.getChannel(); channel.basicAck(deliveryTag, true); System.out.println("死信队列中处理完消息息"); } }); } catch (Exception e) { e.printStackTrace(); } finally { } } }
结果: 消费者一先正常监听到,basicReject为false拒绝后进入死信队列;消费者二监听的死信队列收到消息。
消费者一打出的日志如下:
consumerTag: amq.ctag-0noHs24F0FsGe-dfwwqWNw envelope: Envelope(deliveryTag=1, redeliver=false, exchange=order.exchange, routingKey=routing_key_myQueue) properties: #contentHeader<basic>(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null) 接收到消息: -》 测试消息 拒绝消息, 使之进入死信队列 时间: Sat Nov 07 12:18:44 CST 2020
消费者二打出的日志如下:
时间: Sat Nov 07 12:18:47 CST 2020 consumerTag: amq.ctag-ajYMpMFkXHDiYWkD3XFJ7Q envelope: Envelope(deliveryTag=1, redeliver=false, exchange=order.dead.exchange, routingKey=routing_key_myQueue) properties: #contentHeader<basic>(content-type=null, content-encoding=null, headers={x-death=[{reason=rejected, count=1, exchange=order.exchange, time=Sat Nov 07 01:52:19 CST 2020, routing-keys=[routing_key_myQueue], queue=myQueue}]}, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
接收到消息: -》 测试消息 死信队列中处理完消息息
注意:
进入死信队列之后,headers 加了一些死信相关的信息,包括原队列以及进入死信的原因。
补充:在队列进入死信队列之前也可以修改其routingKey,而且只有在指定x-dead-letter-exchange的前提下才能修改下面属性,否则会报错
(1)修改生产者声明队列的方式,如下:
// 声明一个正常的direct类型的交换机 createChannel.exchangeDeclare("order.exchange", BuiltinExchangeType.DIRECT); // 声明死信交换机为===order.dead.exchange String dlxName = "order.dead.exchange"; createChannel.exchangeDeclare(dlxName, BuiltinExchangeType.DIRECT); // 声明队列并指定死信交换机为上面死信交换机 Map<String, Object> arg = new HashMap<String, Object>(); arg.put("x-dead-letter-exchange", dlxName); // 修改进入死信队列的routingkey,如果不修改会使用默认的routingKey arg.put("x-dead-letter-routing-key", "routing_key_myQueue_dead"); createChannel.queueDeclare("myQueue", true, false, false, arg);
(2)修改监听死信队列的消费者二:
// 队列绑定交换机-channel.queueBind(队列名, 交换机名, 路由key[广播消息设置为空串]) createChannel.queueBind("myQueue", "order.dead.exchange", "routing_key_myQueue_dead");
结果,收到消费者二收到的信息如下:
时间: Sat Nov 07 12:27:08 CST 2020 consumerTag: amq.ctag-THqpEdYH_-iNeCIccgpuaw envelope: Envelope(deliveryTag=1, redeliver=false, exchange=order.dead.exchange, routingKey=routing_key_myQueue_dead) properties: #contentHeader<basic>(content-type=null, content-encoding=null, headers={x-death=[{reason=rejected, count=1, exchange=order.exchange, time=Sat Nov 07 02:00:41 CST 2020, routing-keys=[routing_key_myQueue], queue=myQueue}]}, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null) 接收到消息: -》 测试消息 死信队列中处理完消息
4. 延时队列
有时我们需要用到延时队列,比如说一个场景我们在使用抖音抢购有时候五分钟未下单我们就可以再次抢单。简单的说,用户下单了,库存减一;5分钟未支付,获取到该订单,将商品库存加一。
rabbitMQ本身没提供延时队列,我们可以利用消息的生存时间和死信队列实现延时。
1.生产者:声明队列的消息生存时间、声明死信交换机和路由key
package rabbitmq; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; public class Producer { public static ConnectionFactory getConnectionFactory() { // 创建连接工程,下面给出的是默认的case ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.99.100"); factory.setPort(5672); factory.setUsername("guest"); factory.setPassword("guest"); factory.setVirtualHost("/"); return factory; } public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory connectionFactory = getConnectionFactory(); Connection newConnection = null; Channel createChannel = null; try { newConnection = connectionFactory.newConnection(); createChannel = newConnection.createChannel(); // 声明一个正常的direct类型的交换机 createChannel.exchangeDeclare("order.exchange", BuiltinExchangeType.DIRECT); // 声明死信交换机为===order.dead.exchange String dlxName = "order.dead.exchange"; createChannel.exchangeDeclare(dlxName, BuiltinExchangeType.DIRECT); // 声明队列并指定死信交换机为上面死信交换机 Map<String, Object> arg = new HashMap<String, Object>(); arg.put("x-dead-letter-exchange", dlxName); // 修改进入死信队列的routingkey,如果不修改会使用默认的routingKey arg.put("x-dead-letter-routing-key", "routing_key_myQueue_dead"); // 设置消息的生存时间是1分钟,超时进入死信队列 arg.put("x-message-ttl", 60000); createChannel.queueDeclare("myQueue", true, false, false, arg); // 绑定正常的queue createChannel.queueBind("myQueue", "order.exchange", "routing_key_myQueue"); String message = "订单编号: 001, 订单生成时间: " + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); createChannel.basicPublish("order.exchange", "routing_key_myQueue", null, message.getBytes()); } catch (Exception e) { e.printStackTrace(); } finally { if (createChannel != null) { createChannel.close(); } if (newConnection != null) { newConnection.close(); } } } }
2.消费者: 监听死信交换机和路由key。
package rabbitmq; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeoutException; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DefaultConsumer; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.AMQP.BasicProperties; public class Consumer2 { public static ConnectionFactory getConnectionFactory() { // 创建连接工程,下面给出的是默认的case ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.99.100"); factory.setPort(5672); factory.setUsername("guest"); factory.setPassword("guest"); factory.setVirtualHost("/"); return factory; } public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory connectionFactory = getConnectionFactory(); Connection newConnection = null; Channel createChannel = null; try { newConnection = connectionFactory.newConnection(); createChannel = newConnection.createChannel(); createChannel.queueDeclare("order.expiredQueue", true, false, false, null); // 队列绑定交换机-channel.queueBind(队列名, 交换机名, 路由key[广播消息设置为空串]) createChannel.queueBind("order.expiredQueue", "order.dead.exchange", "routing_key_myQueue_dead"); createChannel.basicConsume("order.expiredQueue", false, "", new DefaultConsumer(createChannel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("当前时间: " + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))); System.out.println("死信队列中收到的订单信息: " + msg); // 处理超时订单,库存加一 // 应答 long deliveryTag = envelope.getDeliveryTag(); Channel channel = this.getChannel(); channel.basicAck(deliveryTag, true); } }); } catch (Exception e) { e.printStackTrace(); } finally { } } }
结果:
当前时间: 2020-11-07 12:52:48
死信队列中收到的订单信息: 订单编号: 001, 订单生成时间: 2020-11-07 12:51:48