上一篇《RabbitMQ:消息何去何从》我们说到当一条消息无法被正确路由到队列时,消息可能被返回给生产者、也可能直接丢弃或者使用备份交换器将消息存储起来。今天说一下RabbitMQ另外三个重要的特性和功能。
1. 过期时间TTL
TTL:Time to Live的简称,即过期时间。RabbitMQ可以对消息和队列设置TTL。
1.1 设置消息的TTL
目前有两种方法可以设置消息的TTL,第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间;第二种方法是对消息本身进行单独设置,每条消息的TTL可以不同。如果两种方法一起使用,则消息的TTL以两者之间较小值为准。
1.1.1 单独设置每条消息的TTL
channel.exchangeDeclare("demo.direct.exchange", "direct", true, false, null);
channel.queueDeclare("demo.queue", true, false, false, null);
channel.queueBind("demo.queue", "demo.direct.exchange", "demo.routingKey");
// 设置消息10s后没有被消费则过期
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties().builder().expiration("10000");
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
builder.build(), "ttl message test.".getBytes());
1.1.2 设置队列中所有消息的TTL
// 设置队列中每条消息的ttl
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl", 10000);
channel.exchangeDeclare("demo.direct.exchange", "direct", true, false, null);
channel.queueDeclare("demo.queue", true, false, false, arguments);
channel.queueBind("demo.queue", "demo.direct.exchange", "demo.routingKey");
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, "ttl message test.".getBytes());
1.2 设置队列的TTL
设置队列的TTL用于控制队列多长时间未被使用后自动删除。需要注意的是:
- 未被使用的意思是:队列上没有任何的消费者,队列也没有重新声明,并且在过期时间段内也未调用过
Basic.Get
命令。 - 如果在指定时间后,满足上面这个未被使用的条件,此时不管队列中有没有积压消息,队列都会自动删除。
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-expires", 10000);
channel.exchangeDeclare("demo.direct.exchange", "direct", true, false, null);
channel.queueDeclare("demo.queue", true, false, false, arguments);
channel.queueBind("demo.queue", "demo.direct.exchange", "demo.routingKey");
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, "ttl message test.".getBytes());
2. 死信队列
当消息在一个队列中变成死信后,它能被重新发送到另外一个交换器中,这个交换器为死信交换器,DLX(Dead-Letter-Exchange)。其绑定的队列就称之为死信队列。
消息变成死信一般是由于以下几种情况:
- 消息被消费者拒绝(
Basic.Reject
/Basic.Nack
),并且设置requeue
参数为false。 - 消息过期。
- 队列达到最大长度。
DLX也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列中的消息满足以上几种情况,RabbitMQ就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到死信队列。
// 创建死信交换器和死信队列
channel.exchangeDeclare("demo.direct.dlx", "direct", true, false, null);
channel.queueDeclare("demo.dlx.queue", true, false, false, null);
channel.queueBind("demo.dlx.queue", "demo.direct.dlx", "demo.routingKey");
Map<String, Object> arguments = new HashMap<>();
// 设置队列中每条消息的ttl
arguments.put("x-message-ttl", 10000);
// 设置死信交换器
arguments.put("x-dead-letter-exchange", "demo.direct.dlx");
channel.exchangeDeclare("demo.direct.exchange", "direct", true, false, null);
channel.queueDeclare("demo.queue", true, false, false, arguments);
channel.queueBind("demo.queue", "demo.direct.exchange", "demo.routingKey");
channel.basicPublish("demo.direct.exchange", "demo.routingKey",
MessageProperties.PERSISTENT_TEXT_PLAIN, "dlx message test.".getBytes());
对于RabbitMQ来说,DLX是一个非常有用的特性。它可以处理异常情况下,消息不能够被消费者正常消费而被置入死信队列中的情况,后续分析程序可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况。
RocketMQ也有死信队列的概念,可参考《RocketMQ:死信队列和消息幂等》
3. 延迟队列
延迟队列存储的对象是对应的延迟消息,所谓“延迟消息”是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等到特定时间后,消费者才能接收到消息进行消费。
延迟队列的使用场景有很多,比如:
- 在订单系统中,一个用户下单之后通常由30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单将进行异常处理,这时就可以使用延迟队列来处理这些订单了。
- 具有定时功能的智能设备,在延迟一定的时间后,再将指令发送给智能设备。比如定时关空调,定时煮饭等。
RabbitMQ本身没有支持延迟队列的功能,不过我们可以通过TTL和DLX很容易实现一个延迟队列。比如在死信队列中的例子,消费者不去监听demo.queue
这个队列,而是直接监听demo.dlx.queue
队列。那么我们每次发送消息到demo.queue
队列,由于设置队列消息的TTL,在10s后,消息进入到死信队列,从而被消费者消费。这就是延迟队列。
在实际场景,我们还能根据不同的需求,设置不同的延迟时间等级。相信这是一个非常实用的实践。