RabbitMQ - 데드 레터 큐, 지연 큐

TTL(Time-To-Live) 및 만료 — RabbitMQ

1. 배달 못한 편지 대기열

데드 레터 교환 — RabbitMQ

데드 레터 대기열:

DLX(Dead-Letter-Exchange)의 풀 네임을 데드 레터 교환(dead letter exchange)이라고 하며, 메시지가 데드 레터가 되었을 때 메시지가 위치한 큐에 x-dead-letter-exchange 파라미터가 있으면 전송 된다 . to x -dead-letter-exchange 값에 해당하는 스위치에서 이 스위치를 데드 레터 스위치라고 하고 이 데드 레터 스위치에 바인드된 큐를 데드 레터 큐라고 합니다.

데드 레터 메시지:

  • 메시지가 거부되고(Basic.Reject 또는 Basic.Nack) requeue 매개변수의 값이 false로 설정됩니다.
  • 메시지 만료(메시지 TTL 만료. TTL : Time To Live의 약자, 즉 만료 시간)
  • 대기열이 최대 길이에 도달했습니다.

만료 메시지:

rabbitmq에는 메시지 만료 시간을 설정하는 두 가지 방법이 있습니다.

  • 첫 번째는 대기열을 설정하는 것입니다 . 이 설정 후 대기열의 모든 메시지는 동일한 만료 시간을 갖습니다.
  • 두 번째는 메시지 자체를 설정하여 각 메시지의 만료 시간이 다릅니다.

이 두 가지 방법을 동시에 사용하는 경우 만료 시간이 짧은 값이 우선합니다. 메시지가 만료 시간에 도달하고 소비되지 않은 경우 해당 메시지는 데드 레터 메시지 가 됩니다.

대기열 설정 : 대기열을 선언할 때 밀리초 단위로 ** x-message-ttl ** 매개변수를 사용합니다.

  • 큐에서 이 속성의 설정은 큐가 처음 선언될 때만 유효하며, 큐가 처음부터 이미 존재하고 이 속성이 없으면 큐를 삭제하고 다시 선언해야 합니다.
  • 대기열의 TTL은 고정 값으로만 ​​설정할 수 있으며, 일단 설정하면 변경할 수 없습니다. 그렇지 않으면 예외가 발생합니다.

단일 메시지 설정 : 메시지 속성의 만료 파라미터 값을 설정하는 것으로 단위는 밀리초입니다.

설명하다:

큐 속성을 설정하는 첫 번째 방법은 메시지가 만료되면 큐에서 지워지는 방법이고, 두 번째 방법은 메시지가 만료되더라도 각 메시지의 만료 여부가 결정되기 때문에 즉시 큐에서 지워지지 않는 방법입니다. 소비자에게 전달되기 직전

 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) 2개의 거래소 생성

 (2) 대기열 myQueue의 데드 레터 대기열에는 속성이 있습니다.

2. 소비자: 
  한 소비자는 일반 대기열을 수신하고 다른 소비자는 배달 못한 편지 대기열을 수신합니다. (바운드 스위치만 다름)

소비자 1: 일반 대기열 듣기

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 {
        }

    }
}

소비자 2: 배달 못한 편지 대기열 듣기

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 {
        }

    }
}

결과: 소비자 1은 정상적으로 수신하고, basicReject는 거짓이며 거부 후 배달 못한 편지 대기열에 들어갑니다. 소비자 2는 배달 못한 편지 대기열을 듣고 메시지를 받습니다.

소비자의 로그 출력은 다음과 같습니다.

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

소비자 2의 로그 출력은 다음과 같습니다.

时间: 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)
接收到消息: -》 测试消息
死信队列中处理完消息息

알아채다:

  배달 못한 편지 대기열에 들어간 후 헤더는 원래 대기열과 배달 못한 편지를 입력한 이유를 포함하여 몇 가지 배달 못한 편지 관련 정보를 추가합니다.

보충: 대기열이 배달 못한 편지 대기열에 들어가기 전에 해당 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) 배달 못한 편지 대기열을 청취하는 소비자 2를 수정합니다.

// 队列绑定交换机-channel.queueBind(队列名, 交换机名, 路由key[广播消息设置为空串])
            createChannel.queueBind("myQueue", "order.dead.exchange", "routing_key_myQueue_dead");

결과적으로 소비자 2가 받은 정보는 다음과 같습니다.

时间: 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)
接收到消息: -》 测试消息
死信队列中处理完消息

둘, 지연 대기열

지연 큐, 즉 메시지가 큐에 들어간 직후에 소비되지 않고 지정된 시간에 도달한 후에야 소비됩니다.

RabbitMQ 자체는 지연 대기열을 제공하지 않으므로 메시지의 생존 시간과 배달 못한 편지 대기열을 사용하여 지연을 달성할 수 있습니다.

일반적인 적용 시나리오는 30분 이내에 결제 없이 주문을 종료하는 것이며, 24시간 이내에 청구서가 확인되지 않고 알림 메시지가 전송되는 시나리오도 있습니다.

지연 대기열 플러그인 설치

2.1.1, yml 구성

spring:
    rabbitmq:
        host: 192.168.99.12
        port: 5672
        username: guest
        password: guest
        # 发送确认
        publisher-confirms: true
        # 路由失败回调
        publisher-returns: true
        template:
            # 必须设置成true 消息路由失败通知监听者,false 将消息丢弃
            mandatory: true
        #消费端
        listener:
            simple:
                # 每次从RabbitMQ获取的消息数量
                prefetch: 1
                default-requeue-rejected: false
                # 每个队列启动的消费者数量
                concurrency: 1
                # 每个队列最大的消费者数量
                max-concurrency: 1
                # 签收模式为手动签收-那么需要在代码中手动ACK
                acknowledge-mode: manual
#邮件队列
email:
    queue:
        name: demo.email

#邮件交换器名称
exchange:
    name: demoTopicExchange

#死信队列
dead:
    letter:
        queue:
            name: demo.dead.letter
        exchange:
            name: demoDeadLetterTopicExchange

#延时队列
delay:
    queue:
        name: demo.delay
    exchange:
        name: demoDelayTopicExchange

2.1.2 지연 대기열 구성

/**
 * rabbitmq 配置
 *
 * @author DUCHONG
 * @since 2020-08-23 14:05
 **/
@Configuration
@Slf4j
public class RabbitmqConfig {


    @Value("${email.queue.name}")
    private String emailQueue;
    @Value("${exchange.name}")
    private String topicExchange;
    @Value("${dead.letter.queue.name}")
    private String deadLetterQueue;
    @Value("${dead.letter.exchange.name}")
    private String deadLetterExchange;
    @Value("${delay.queue.name}")
    private String delayQueue;
    @Value("${delay.exchange.name}")
    private String delayExchange;

    @Bean
    public Queue emailQueue() {

        Map<String, Object> arguments = new HashMap<>(2);
        // 绑定死信交换机
        arguments.put("x-dead-letter-exchange", deadLetterExchange);
        // 绑定死信的路由key
        arguments.put("x-dead-letter-routing-key", deadLetterQueue+".#");

        return new Queue(emailQueue,true,false,false,arguments);
    }


    @Bean
    TopicExchange emailExchange() {
        return new TopicExchange(topicExchange);
    }


    @Bean
    Binding bindingEmailQueue() {
        return BindingBuilder.bind(emailQueue()).to(emailExchange()).with(emailQueue+".#");
    }


    //私信队列和交换器
    @Bean
    public Queue deadLetterQueue() {
        return new Queue(deadLetterQueue);
    }

    @Bean
    TopicExchange deadLetterExchange() {
        return new TopicExchange(deadLetterExchange);
    }

    @Bean
    Binding bindingDeadLetterQueue() {
        return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(deadLetterQueue+".#");
    }
    //延时队列
    @Bean
    public Queue delayQueue() {
        return new Queue(delayQueue);
    }

    @Bean
    CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "topic");
        //参数二为类型:必须是x-delayed-message
        return new CustomExchange(delayExchange, "x-delayed-message", true, false, args);

    }

    @Bean
    Binding bindingDelayQueue() {
        return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(delayQueue+".#").noargs();
    }
}

2.2 메시지 발신자

30분은 너무 길다, 효과를 보려면 2분 지연

@Configuration
@EnableScheduling
@Slf4j
public class ScheduleController {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Value("${exchange.name}")
    private String topicExchange;

    @Value("${delay.exchange.name}")
    private String delayTopicExchange;

    @Scheduled(cron = "0 0/1 * * * ?")
    public void sendEmailMessage() {

        String msg = RandomStringUtils.randomAlphanumeric(8);
        JSONObject email=new JSONObject();
        email.put("content",msg);
        email.put("to","[email protected]");
        CorrelationData correlationData=new CorrelationData(UUID.randomUUID().toString());
        rabbitTemplate.convertAndSend(topicExchange,"demo.email.x",email.toJSONString(),correlationData);
        log.info("---发送 email 消息---{}---messageId---{}",email,correlationData.getId());
    }


    @Scheduled(cron = "0 0/1 * * * ?")
    public void sendDelayOrderMessage() throws Exception{

        //订单号 id实际是保存订单后返回的,这里用uuid代替
        String orderId = UUID.randomUUID().toString();
        // 模拟订单信息
        JSONObject order=new JSONObject();
        order.put("orderId",orderId);
        order.put("goodsName","vip充值");
        order.put("orderAmount","99.00");
        CorrelationData correlationData=new CorrelationData(orderId);
        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setMessageId(orderId);
        //30分钟时间太长,这里延时120s消费
        messageProperties.setHeader("x-delay", 120000);
        Message message = new Message(order.toJSONString().getBytes(CharEncoding.UTF_8), messageProperties);

        rabbitTemplate.convertAndSend(delayTopicExchange,"demo.delay.x",message,correlationData);

        log.info("---发送 order 消息---{}---orderId---{}",order,correlationData.getId());
        //睡一会,为了看延迟效果
        TimeUnit.MINUTES.sleep(10);
    }
}

2.3 메시지 소비자

@Component
@Slf4j
public class MessageHandler {


    /**
     * 邮件发送
     * @param message
     * @param channel
     * @param headers
     * @throws IOException
     */
    @RabbitListener(queues ="demo.email")
    @RabbitHandler
    public void handleEmailMessage(Message message, Channel channel, @Headers Map<String,Object> headers) throws IOException {

        try {

            String msg=new String(message.getBody(), CharEncoding.UTF_8);
            JSONObject jsonObject = JSON.parseObject(msg);
            jsonObject.put("messageId",headers.get("spring_returned_message_correlation"));
            log.info("---接受到消息---{}",jsonObject);
			//主动异常
			int m=1/0;
            //手动签收
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }
        catch (Exception e) {
            log.info("handleEmailMessage捕获到异常,拒绝重新入队---消息ID---{}", headers.get("spring_returned_message_correlation"));
            //异常,ture 重新入队,或者false,进入死信队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);

        }
    }

    /**
     * 死信消费者,自动签收开启状态下,超过重试次数,或者手动签收,reject或者Nack
     * @param message
     */
    @RabbitListener(queues = "demo.dead.letter")
    public void handleDeadLetterMessage(Message message, Channel channel,@Headers Map<String,Object> headers) throws IOException {

        //可以考虑数据库记录,每次进来查数量,达到一定的数量,进行预警,人工介入处理
        log.info("接收到死信消息:---{}---消息ID---{}", new String(message.getBody()),headers.get("spring_returned_message_correlation"));
		//回复ack
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }

    /**
     * 延时队列消费
     * @param message
     * @param channel
     * @param headers
     * @throws IOException
     */
    @RabbitListener(queues ="demo.delay")
    @RabbitHandler
    public void handleOrderDelayMessage(Message message, Channel channel, @Headers Map<String,Object> headers) throws IOException {

        try {

            String msg=new String(message.getBody(), CharEncoding.UTF_8);
            JSONObject jsonObject = JSON.parseObject(msg);
            log.info("---接受到订单消息---orderId---{}",message.getMessageProperties().getMessageId());
            log.info("---订单信息---order---{}",jsonObject);
            //业务逻辑,根据订单id获取订单信息,如果还未支付,设置关闭状态,如果已支付,不做任何处理
            //手动签收
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }
        catch (Exception e) {
            log.info("handleOrderDelayMessage捕获到异常,重新入队---orderId---{}", headers.get("spring_returned_message_correlation"));
            //异常,ture 重新入队,或者false,进入死信队列
            channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);

        }
    }

}

2.4 결과

실행 결과는 동일한 주문 번호를 가진 메시지가 보낸 후 불과 2분 만에 소비자에게 수신되었음을 보여 주며 이는 예상과 일치합니다.

https://www.cnblogs.com/geekdc/p/13550620.html

Message Queue RabbitMQ (5): 배달 못한 편지 대기열 및 지연 대기열

Rabbitmq의 지연 큐와 데드 레터 큐_데드 레터 큐와 지연 큐의 차이점_zhuwenaptx's Blog-CSDN Blog

RabbitMQ의 데드 레터 큐 및 지연 큐 - 프로그래머 구함

RabbitMQ 배달 못한 편지 대기열 및 지연 대기열_51CTO Blog_rabbitmq 지연 대기열

추천

출처blog.csdn.net/MinggeQingchun/article/details/130482153