概念

与普通队列的区别是,在延迟队列中的消息不会被消费者立马消费,而是延迟一段时间后,才会被消费。

场景

用户选择完商品后,提交订单。订单生成之后,在规定时间内,检验订单的支付状态,如果订单超过规定时间仍然没有支付,那么需要关闭此订单。

原理

用到了RabbitMQ的两个特性:Time-To-Live Extensions、Dead Letter Exchange。

Time-To-Live Extensions

TTL。TTL是RabbitMQ中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间。如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。
在声明队列时设置

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 60000);
channel.queueDeclare("myqueue", false, false, false, args);

设置每条消息的ttl

byte[] messageBodyBytes = "Hello, world!".getBytes();
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                                   .expiration("60000")
                                   .build();
channel.basicPublish("my-exchange", "routing-key", properties, messageBodyBytes);

Dead Letter Exchange

DLX。队列中的消息有可能是“死信”,当下面的情况发生的时候,消息就被认为是“死信”。
1、The message is negatively acknowledged by a consumer using basic.reject or basic.nack with requeue parameter set to false.
2、The message expires due to per-message TTL; or
3、The message is dropped because its queue exceeded a length limit.
设置方式

channel.exchangeDeclare("some.exchange.name", "direct");

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "some.exchange.name");
channel.queueDeclare("myqueue", false, false, false, args);

在对消息进行死信处理的时候,还可以指定一个routing key。如果不指定,将使用消息本身的routing key。

args.put("x-dead-letter-routing-key", "some-routing-key");

指定后,在RabbitMQ管理界面,队列名称右边,会看到DLK特征。

centos安装rabbitmq 延迟消息插件 rabbitmq延迟队列原理_发送消息

Demo

接下来,我们将使用Spring Boot来进行演示,其中涉及的Exchange以及Queue的总体设计如下图:

centos安装rabbitmq 延迟消息插件 rabbitmq延迟队列原理_Time_02

引入依赖包

<!-- RabbitMQ -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

RabbitMQ配置

spring:
  # RabbitMQ
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    listener:
      type: simple
      simple:
        default-requeue-rejected: false
        acknowledge-mode: none

ExchangeConfig

/**
 * <p>交换机配置类</p>
 *
 */
@Configuration
public class ExchangeConfig {

    public static final String DL_MESSAGE_BUSINESS_EXCHANGE = "dl.message.business.exchange";
    public static final String DL_MESSAGE_DEAD_LETTER_EXCHANGE = "dl.message.dead.letter.exchange";

    // 业务Exchange
    @Bean("dlMessageBusinessExchange")
    public TopicExchange dlMessageOutpushBusinessExchange(){
        TopicExchange topicExchange = new TopicExchange(DL_MESSAGE_BUSINESS_EXCHANGE,false,false);
        return topicExchange;
    }

    // 死信Exchange
    @Bean("dlMessageDeadLetterExchange")
    public DirectExchange dlMessageOutpushDeadletterExchange(){
        DirectExchange directExchange = new DirectExchange(DL_MESSAGE_DEAD_LETTER_EXCHANGE);
        return directExchange;
    }

}

QueueConfig

/**
 * <p>队列配置类</p>
 *
 */
@Configuration
public class QueueConfig {

    public static final String DL_MESSAGE_BUSINESS_QUEUE = "dl.message.business.queue";
    public static final String DL_MESSAGE_DEAD_LETTER_QUEUE = "dl.message.dead.letter.queue";

    public static final String DL_MESSAGE_DEAD_LETTER_ROUTING = "dl.message.dead.letter";

    @Bean("dlMessageBusinessQueue")
    public Queue dlMessageOutpushBusinessQueueChaotao(){
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", ExchangeConfig.DL_MESSAGE_DEAD_LETTER_EXCHANGE);
        args.put("x-dead-letter-routing-key", DL_MESSAGE_DEAD_LETTER_ROUTING);

        Queue queue = QueueBuilder.durable(DL_MESSAGE_BUSINESS_QUEUE).withArguments(args).build();
        return queue;
    }

    @Bean("dlMessageDeadLetterQueue")
    public Queue dlMessageOutpushDeadLetterQueueChaotao(){
        Queue queue = new Queue(DL_MESSAGE_DEAD_LETTER_QUEUE);
        return queue;
    }
}

RabbitMqConfig

@Configuration
public class RabbitMqConfig {

    /**
     * Bindings Key
     */
    public static final String DL_MESSAGE_BUSINESS_BINDING = "dl.message.business.#";
    public static final String DL_MESSAGE_DEAD_LETTER_BINDING = "dl.message.dead.letter";


    @Value("${spring.rabbitmq.host}")
    private String host;

    @Value("${spring.rabbitmq.port}")
    private int port;

    @Value("${spring.rabbitmq.username}")
    private String username;

    @Value("${spring.rabbitmq.password}")
    private String password;

    @Bean
    public ConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
        connectionFactory.setUsername(username);
        connectionFactory.setPassword(password);
        connectionFactory.setVirtualHost("/");
        return connectionFactory;
    }

    @Bean
    public AsyncRabbitTemplate asyncRabbitTemplate(RabbitTemplate rabbitTemplate){
        return new AsyncRabbitTemplate(rabbitTemplate);
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public RabbitTemplate rabbitTemplate() {
        RabbitTemplate template = new RabbitTemplate(connectionFactory());
        return template;
    }

    @Bean
    public Binding businessBinding(@Qualifier("dlMessageBusinessQueue") Queue queue,
                                    @Qualifier("dlMessageBusinessExchange") TopicExchange exchange){
        Binding binding = BindingBuilder.bind(queue).to(exchange).with(DL_MESSAGE_BUSINESS_BINDING);
        return binding;
    }

    @Bean
    public Binding deadLetterBinding(@Qualifier("dlMessageDeadLetterQueue") Queue queue,
                                      @Qualifier("dlMessageDeadLetterExchange") DirectExchange exchange){
        Binding binding = BindingBuilder.bind(queue).to(exchange).with(DL_MESSAGE_DEAD_LETTER_BINDING);
        return binding;
    }
}

消费者

@Component
public class TestRabbitMQListener {

    private final Logger logger = LoggerFactory.getLogger(TestRabbitMQListener.class);

    @RabbitListener(queues = QueueConfig.DL_MESSAGE_DEAD_LETTER_QUEUE)
    public void receiveDeadLetter(Message message, Channel channel) throws IOException {
        System.out.println("receive dead letter=" + new String(message.getBody()));
    }
}

编写一个Rest接口,用于向队列中发送消息

@RestController
public class TesRabbitMQController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //routing key
    public static final String DL_MESSAGE_BUSINESS_ROUTING_KEY = "dl.message.business.test.xxx";

    @GetMapping("send")
    public String sendMsg(@RequestParam("msg") String msg){

        MessageProperties messageProperties = new MessageProperties();
        //设置单条消息的超时时间为10秒
        messageProperties.setExpiration("10000");
        Message message = new Message(msg.getBytes(), messageProperties);

        rabbitTemplate.convertAndSend(ExchangeConfig.DL_MESSAGE_BUSINESS_EXCHANGE, DL_MESSAGE_BUSINESS_ROUTING_KEY, message);
        return "ok";
    }

}

测试

以上代码编写完成后,启动我们的应用,会看见创建了两个Exchange

centos安装rabbitmq 延迟消息插件 rabbitmq延迟队列原理_rabbitmq_03


两个Queue

centos安装rabbitmq 延迟消息插件 rabbitmq延迟队列原理_spring_04


调用Rest接口后,会往延时队列中进行发送消息。

centos安装rabbitmq 延迟消息插件 rabbitmq延迟队列原理_rabbitmq_05


由于消息设置了10秒超时,所以10秒之后,此消息会进入死信队列。

centos安装rabbitmq 延迟消息插件 rabbitmq延迟队列原理_spring_06


我们的消费者监听死信队列,最终会在控制台打印出如下信息

receive dead letter=test