RabbitMQ(四)延迟消息

297 阅读2分钟

1.死信交换机和死信队列实现延迟消息

1.1死信交换机和死信队列

什么是死信?

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息满了,无法投递

如果这个包含死信的队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,检查DLX)。

image-20210718174506856.png

在失败重试策略中,默认的RejectAndDontRequeueRecoverer会在本地重试次数耗尽后,发送reject给RabbitMQ,消息变成死信,被丢弃。

我们可以给simple.queue添加一个死信交换机,给死信交换机绑定一个队列。这样消息变成死信后也不会丢弃,而是最终投递到死信交换机,路由到与死信交换机绑定的队列。

1.2 TTL实现延迟消息

一个队列中的消息如果超时未消费,则会变为死信,超时分为两种情况:

  • 消息所在的队列设置了超时时间
  • 消息本身设置了超时时间

image-20210718182643311.png

死信交换机配置:

@Configuration
public class TTLMessageConfig {

    @Bean
    public DirectExchange ttlDirectExchange(){
        //声明交换机
        return new DirectExchange("ttl.direct");
    }

    @Bean
    public Queue ttlQueue(){
        return QueueBuilder
                .durable("ttl.queue")
                .ttl(10000)//队列设置超时时间
                .deadLetterExchange("dl.direct")//队列绑定死信交换机 超时会投递到该交换机
                .deadLetterRoutingKey("dl")// 设置死信routingkey
                .build();
    }

    @Bean
    public Binding ttlBinding(){
        //交换机和队列绑定
        return BindingBuilder.bind(ttlQueue()).to(ttlDirectExchange()).with("ttl");
    }
}

publisher:


/**
 * 死信交换机发送延迟消息
 * 死信队列设置的ttl 和 消息设置的setExpiration 较小的生效
 */
@Test
public void testTTLMessage() {
    Map<String, String> msg = new HashMap<>();
    msg.put("msg", "hello, ttl messsage");
    // 1.准备消息
    Message message = MessageBuilder
            .withBody(JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8))
            .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
            .setExpiration("5000").build();
    // 2.发送消息
    rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
    // 3.记录日志
    log.info("消息已经成功发送!");
}

consumer:

//设置死信交换机绑定死信队列并监听死信消息
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "dl.queue", durable = "true"),
        exchange = @Exchange(name = "dl.direct"),
        key = "dl"
))
public void listenDlQueue(Message msg)  {
    log.info("消费者接收到了dl.queue的延迟消息:"+ new String(msg.getBody(), StandardCharsets.UTF_8));
}

2.延迟队列实现

2.1 DelayExchange原理

DelayExchange是RabbitMQ的官方也推出了一个插件,原生支持延迟队列效果。安装DelayExchange步骤不做赘述,直接记录使用过程。

DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:

  • 接收消息
  • 判断消息是否具备x-delay属性
  • 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
  • 返回routing not found结果给消息发送者
  • x-delay时间到期后,重新投递消息到指定队列

2.2 代码实现

publisher:

/**
 * 测试延迟队列发送消息
 * @throws InterruptedException
 */
@Test
public void testSendDelayMessage() {
    Map<String, String> msg = new HashMap<>();
    msg.put("msg", "hello, delay messsage");
    // 1.准备消息
    Message message = MessageBuilder
            .withBody(JSONObject.toJSONString(msg).getBytes(StandardCharsets.UTF_8))
            .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
            .setHeader("x-delay", 10000) // 设置延迟10秒
            .build();
    // 2.准备CorrelationData
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 2.2.准备ConfirmCallback
    correlationData.getFuture().addCallback(result -> {
        // 判断结果
        if (result.isAck()) {
            // ACK
            log.info("消息成功投递到交换机!消息ID: {}", correlationData.getId());
        } else {
            // NACK
            log.error("消息投递到交换机失败!消息ID:{}", correlationData.getId());
            // 重发消息
        }
    }, ex -> {
        // 记录日志
        log.error("消息发送失败!", ex);
        // 重发消息
    });
    // 3.发送消息
    rabbitTemplate.convertAndSend("delay.direct", "delay", message, correlationData);

    log.info("发送消息完成");
}

consumer:

//声明并监听延迟队列   delayed = "true"设置延迟交换机
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "delay.queue", durable = "true"),
        exchange = @Exchange(name = "delay.direct", delayed = "true"),
        key = "delay"
))
public void listenDelayExchange(Message msg) {
    log.info("消费者接收到了delay.queue的延迟消息:"+new String(msg.getBody(),StandardCharsets.UTF_8));
}