RabbitMQ如何实现延时队列

731 阅读6分钟

✨这里是第七人格的博客✨小七,欢迎您的到来~✨

🍅系列专栏:无🍅

✈️本篇内容: rabbitmq如何实现延时队列✈️

🍱本篇收录完整代码地址:gitee.com/diqirenge/s…🍱

一、楔子

延时队列在许多业务场景中都有着广泛的运用。但可惜的是在RabbitMQ中并未提供延迟队列功能。这里小七结合工作所用,列出2种实现方式。

(1)使用TTL+死信队列组合实现延迟队列的效果。

(2)使用RabbitMQ官方延迟插件,实现延时队列效果。

二、使用TTL+死信队列组合实现延迟队列的效果

使用这种方式实现延时队列,我们首先要理清楚2个概念。TTL和死信队列。

1、TTL

TTL 全称 Time To Live(存活时间/过期时间)。当消息到达存活时间后,还没有被消费,会被自动清除。RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。可以类比redis的TTL。

2、死信队列

死信队列,英文缩写:DLX  。Dead Letter Exchange(死信交换机)。那如何让一个普通的队列成为死信队列呢?

消息成为死信的三种情况:

(1) 队列消息长度到达限制;

(2)消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;

(3) 原队列存在消息过期设置,消息到达超时时间未被消费;

3、逻辑整理

结合TTL和死信队列,我们可以做以下思考

(1)创建队列b(普通队列)和队列a(死信队列)分别绑定交换机d,并且提供监听队列b(普通队列)的消费者c,队列a(死信队列)不提供消费者。

(2)生产者发送包含TTL的消息到队列a(死信队列)。

(3)队列a(死信队列)过期后,经由交换机d转发到队列b(普通队列)。

(4)消费者c消费队列b(普通队列)的信息。

如图:

图片

4、代码实现

有一个经典的业务场景可以使用延迟队列:下单后,30分钟未支付,取消订单。这里小七基于这个需求给出代码实现。

测试消息体

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageStruct implements Serializable {
    private static final long serialVersionUID = 392365881428311040L;
    private String message;
}

RabbitMQ常量池

public interface RabbitConsts {
    //=====================TTL+死信队列实现======================//
    /**
     * 声明了队列里的死信转发到的DLX名称
     */
    String X_DEAD_LETTER_EXCHANGE = "x-dead-letter-exchange";
    /**
     * 声明了这些死信在转发时携带的 routing-key 名称
     */
    String X_DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
    /**
     * 消息过期后会转到该队列中,需要消费者监听并消费
     */
    String CANCELORDER_CONSUME_QUEUE = "cancelOrderConsumeQueue";
    /**
     * 声明了生产者在发送消息时携带的 routing-key 名称
     */
    String CANCELORDER_SEND_ROUTINGKEY = "cancelOrderSendRoutingKey";
    /**
     * 声明了生产者在发送消息时携带的 Queue 名称
     */
    String CANCELORDER_QUEUE = "cancelOrderSendQueue";
}

配置文件

@Slf4j
@Configuration
public class RabbitMqConfig {
    @Bean
    public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("消息发送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause));
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message));
        return rabbitTemplate;
    }
@Bean
public TopicExchange synCancelOrderDelayExchange(){
    TopicExchange topicExchange = new TopicExchange(RabbitConsts.X_DEAD_LETTER_EXCHANGE,true,false);
    return topicExchange;
}
@Bean
public Queue synCancelOrderDelaySendQueue() {
    /**
     durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
     auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
     exclusive  表示该消息队列是否只在当前connection生效,默认是false
     */
    Map<String, Object> params = new HashMap<>();
    // x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
    params.put(RabbitConsts.X_DEAD_LETTER_EXCHANGE, RabbitConsts.X_DEAD_LETTER_EXCHANGE);
    // x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
    params.put(RabbitConsts.X_DEAD_LETTER_ROUTING_KEY,RabbitConsts.X_DEAD_LETTER_ROUTING_KEY);
    return new Queue(RabbitConsts.CANCELORDER_QUEUE, true, false, false,params);
}
@Bean
public Queue synCancelOrderDelayConsumeQueue() {
    /**
     durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
     auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
     exclusive  表示该消息队列是否只在当前connection生效,默认是false
     死信队列里的消息过期后会转到该队列中
     */
    return new Queue(RabbitConsts.CANCELORDER_CONSUME_QUEUE, true, false, false);
}
@Bean
public Binding bindingCancelOrderSend() {
    return BindingBuilder.bind(synCancelOrderDelaySendQueue()).to(synCancelOrderDelayExchange()).with(RabbitConsts.CANCELORDER_SEND_ROUTINGKEY);
}
@Bean
public Binding bindingCancelOrderConsume() {
    return BindingBuilder.bind(synCancelOrderDelayConsumeQueue()).to(synCancelOrderDelayExchange()).with(RabbitConsts.X_DEAD_LETTER_ROUTING_KEY);
}
}

消费者

@Slf4j
@Component
@RabbitListener(queues = RabbitConsts.CANCELORDER_CONSUME_QUEUE)
public class CancelOrderConsumer {
    @RabbitHandler
    public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
        //  如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
        final long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            log.info("订单自动取消延迟队列,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
            // 通知 MQ 消息已被成功消费,可以ACK了
            channel.basicAck(deliveryTag, false);
        } catch (IOException e) {
            try {
                // 处理失败,重新压入MQ
                channel.basicRecover();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

生产者

@RestController
@Api(tags = "测试SpringBoot整合进行各种工作模式信息的发送", value = "测试SpringBoot整合进行各种工作模式信息的发送")
public class ProducerController {
   @Autowired
   private RabbitTemplate rabbitTemplate;
   /**
    * 下单完成会发送消息到延迟队列 用于自动取消订单
    */
   @ApiOperation(value="测试延迟队列发送接口(订单取消)",notes="测试延迟队列发送(订单取消)")
   @GetMapping(value="/synSendCancelOrderMessage")
   public void synSendCancelOrderMessage(){
      rabbitTemplate.convertAndSend(RabbitConsts.X_DEAD_LETTER_EXCHANGE,
            RabbitConsts.CANCELORDER_SEND_ROUTINGKEY, new MessageStruct("自动取消订单"), message -> {
               message.getMessageProperties().setExpiration(String.valueOf(10 * 1000));
               return message;
            });
   }
}

完整项目代码地址:gitee.com/diqirenge/s…

三、使用RabbitMQ官方延迟插件,实现延时队列效果。

基于TTL+死信队列的组合虽然可以实现延迟队列,但是对于平常的开发太过繁琐。于是小七在rabbitmq官方,发现了官方推荐的一款延迟队列插件,让延时队列的实现同一般队列那么简单。

测试消息体

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageStruct implements Serializable {
    private static final long serialVersionUID = 392365881428311040L;
    private String message;
}

RabbitMQ常量池

public interface RabbitConsts {
/**
 * 延迟队列
 */
String DELAY_QUEUE = "delay.queue";
/**
 * 延迟队列交换器
 */
String DELAY_MODE_QUEUE = "delay.mode";
}

配置文件

@Slf4j
@Configuration
public class RabbitMqConfig {
    @Bean
    public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory) {
        connectionFactory.setPublisherConfirms(true);
        connectionFactory.setPublisherReturns(true);
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("消息发送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause));
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info("消息丢失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message));
        return rabbitTemplate;
    }
   //===================延迟队列插件实现start====================//
/**
 * 延迟队列
 */
@Bean
public Queue delayQueue() {
    return new Queue(RabbitConsts.DELAY_QUEUE, true);
}
/**
 * 延迟队列交换器, x-delayed-type 和 x-delayed-message 固定
 */
@Bean
public CustomExchange delayExchange() {
    Map<String, Object> args = new HashMap<>(16);
    args.put("x-delayed-type", "direct");
    return new CustomExchange(RabbitConsts.DELAY_MODE_QUEUE, "x-delayed-message", true, false, args);
}
/**
 * 延迟队列绑定自定义交换器
 *
 * @param delayQueue    队列
 * @param delayExchange 延迟交换器
 */
@Bean
public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
    return BindingBuilder.bind(delayQueue).to(delayExchange).with(RabbitConsts.DELAY_QUEUE).noargs();
}
//===================延迟队列插件实现end====================/
}

消费者

@Slf4j
@Component
@RabbitListener(queues = RabbitConsts.DELAY_QUEUE)
public class DelayQueueConsumer {
    @RabbitHandler
    public void directHandlerManualAck(MessageStruct messageStruct, Message message, Channel channel) {
        //  如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
        final long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            log.info("延迟队列,手动ACK,接收消息:{}", JSONUtil.toJsonStr(messageStruct));
            // 通知 MQ 消息已被成功消费,可以ACK了
            channel.basicAck(deliveryTag, false);
        } catch (IOException e) {
            try {
                // 处理失败,重新压入MQ
                channel.basicRecover();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

生产者

@RestController
@Api(tags = "测试SpringBoot整合进行各种工作模式信息的发送", value = "测试SpringBoot整合进行各种工作模式信息的发送")
public class ProducerController {
   @Autowired
   private RabbitTemplate rabbitTemplate;
   /**
 * 测试延迟队列发送
 */
@ApiOperation(value="测试延迟队列发送接口",notes="测试延迟队列发送")
@GetMapping(value="/sendDelay")
public void sendDelay() {
   rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message, delay 5s, " + DateUtil
         .date()), message -> {
      message.getMessageProperties().setHeader("x-delay", 5000);
      return message;
   });
   rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message,  delay 2s, " + DateUtil
         .date()), message -> {
      message.getMessageProperties().setHeader("x-delay", 2000);
      return message;
   });
   rabbitTemplate.convertAndSend(RabbitConsts.DELAY_MODE_QUEUE, RabbitConsts.DELAY_QUEUE, new MessageStruct("delay message,  delay 8s, " + DateUtil
         .date()), message -> {
      message.getMessageProperties().setHeader("x-delay", 8000);
      return message;
   });
}
}

完整项目代码地址:gitee.com/diqirenge/s…