如果使用消息拒绝机制,同时 requeue 参数设置为 false时,消息丢失了,这点作为程序员我们不能忍。所以 RabbitMQ作为一个高级消息中间件,提出了死信交换器的概念,死信,意思就是死了的信息。这种交换器专门处理死了的信息(被拒绝可以重新投递的信息不能算死的)。

死信交换器是 RabbitMQ 对 AMQP 规范的一个扩展,往往用在对问题消息的诊断上(主要针对消费者),还有延时队列的功能。

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

  • 消息被拒绝,并且设置 requeue 参数为 false
  • 消息过期(默认情况下 Rabbit 中的消息不过期,但是可以设置队列的过期时间和消息的过期时间以达到消息过期的效果)
  • 队列达到最大长度(一般当设置了最大队列长度或大小并达到最大值时)

当满足上面三种情况时,消息会变成死信消息,并通过死信交换机投递到相应的队列中。我们只需要监听相应队列,就可以对死信消息进行最后的处理。

RabbitMQ-死信交换机和死信队列_队列queue

下面我们演示一下如何利用死信队列完成未支付订单的超时处理功能。

首先生产者生产一条1分钟后超时的订单消息到正常交换机exchange-a中,消息匹配到队列queue-a,但一分钟后仍未消费。消息会被投递到死信交换机dlx-exchange中,并发送到私信队列中.死信队列dlx-queue的消费者拿到消息后,根据消息去查询订单的状态,如果仍然是未支付状态,将订单状态更新为超时状态。

RabbitMQ-死信交换机和死信队列_主键_02

代码演示:

声明正常交换机:exchange-a

public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();
        //声明正常交换机exchange-a
        channel.exchangeDeclare("exchange-a", BuiltinExchangeType.DIRECT);

        //设置超时时间为10s
        AMQP.BasicProperties props = new AMQP.BasicProperties().builder().expiration("10000").build();

        //发送消息到正常交换机exchange-a中,消息体可以包含订单的主键信息,方便消费者后续根据信息去查找订单
        System.out.println(new Date());
        channel.basicPublish("exchange-a", "order", props, "999".getBytes());
        channel.close();
        connection.close();
    }

声明正常队列queue-a,并指定消息死信时的死信交换机名称,但没有消费者,这样子一直没法被消费的话,消息就会过期。

public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();
        //声明一个正常队列queue-a
        String queueName = "queue-a";
        Map<String, Object> props = new HashMap<String, Object>();
        props.put("x-dead-letter-exchange", DlxConsumer.DLX_EXCHANGE);
        //声明队列时最后一个参数放入一个map,当队列中的消息变为死信时,会重新投递到map中指定的死信队列中
        channel.queueDeclare(queueName, false, false, false, props);
        //绑定正常队列
        channel.queueBind(queueName, "exchange-a", "order");
    }

声明死信交换机,声明死信队列并绑定死信交换机,消费死信队列的消息。

public static final String DLX_EXCHANGE = "dlx_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        //声明死信交换机
        channel.exchangeDeclare(DlxConsumer.DLX_EXCHANGE,
                BuiltinExchangeType.TOPIC);

        //声明死信队列,绑定死信交换机
        String queueName = "dlx_queue";
        channel.queueDeclare(queueName, false, false, false, null);
        channel.queueBind(queueName,
                DlxConsumer.DLX_EXCHANGE, "#");

        //消费死信队列中的消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                System.out.println("收到死信消息,超时订单id为  " + new String(body));
                // 查询数据库,检查并更改订单状态........
            }
        });
    }

开始运行:

生产消息到exchange-a中

RabbitMQ-死信交换机和死信队列_System_03

到web客户端,查看正常队列queue-a的消息,发现有一条10s后过期的时间

RabbitMQ-死信交换机和死信队列_主键_04

过完10s我们去死信队列的消费者查看,是否有消息

RabbitMQ-死信交换机和死信队列_System_05

发现收到消息的时间刚好是10s之后

ok,大功告成啦