Spring 利用RabbitMQ实现延迟队列

154 阅读6分钟

说明

最近开始整理以前的笔记,准备把记录的小小知识点慢慢的搬到博客上来。

申明是延迟队列

顾名思义,延迟队列就是延迟队列指的是进入消息队列的消息如果在消费者能正常消费的情况下,立马可以被消费的队列。(消费队列堆积等无法正常消费的情况这里暂不考虑)。

整体思路

利用RabbitMQ实现延迟队列,主要用到RabbitMq的两个特性Time-To-Live Extensions(有效期扩展)、Dead Letter Exchanges(死信路由);一般情况下的消息通过路由转到队列后会马上被消费者消费,延迟队列就是延迟一定时间才被消费的队别。

Time-To-Live Extensions

RabbitMQ可以为消息、队列设置ttl属性(过期时间),ttl表明了一条消息可在队列中存活的最大时间,单位为毫秒 注意ttl扩展属性队列、消息都有,如果消息、消息投递的队列都设置的ttl属性则较小值会生效

Dead Letter Exchanges

一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列。消息满足'死亡'有下面几种情况

一个消息被Consumer拒收了,且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用 消息因为设置了TTL而过期 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上

如果队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新publish到Dead Letter Exchange,通过Dead Letter Exchange路由到其他队列。

延迟消费流程

几个概念

交换机(路由)

Exchange分为DirectExchange、FanoutExchange、TopicExchange、HeadersExchange; DirectExchange 直连类型,exchange根据routing-key路由到对应的queue;routing- key必须绝对匹配;FanoutExchange 广播类型不需要routing-key消息直接路由到交换 机绑定的queue;TopicExchange主题类型,exchange模糊匹配到指定的routing-key消 息路由到这些routing-key对应的queue;routing-key可以模糊匹配 现有a:fade.goods/b:fade.order.195.# c:fade534.194生产者:routing-key为:fade.* 则会匹配a/b绑定的queue;星号(*) 只能匹配一个单次,井号(#) 可以匹配多个(零个);在RabbitMq中消息都先会投递到Exchange中然后在路由到对应关系绑定的Queue中

队列

Queue,RabbtiMq中用来存储消息的单元

Binding

Exchange与Queue是通过Bingding绑定联系起来的

routing key

Exchange在转发消息到Queue会通过routing-key在找与之匹配的Queue,与之对应的Queue都会接受到消息

具体示例

应用

RabbitMQ手工确认模式问题,如果在配置RabbitMQ时指定消息确认模式手工确认(AcknowledgeMode.MANUAL),则RabbitTemplate的Scope必须设置为PROTOTYPE,否则消息只会处理一次

@Bean
public DirectExchange defaultExchange() {
    System.out.println("申明交换机");
    return new DirectExchange("fade-exhcange");
}

/** 队列绑定交换机 */
* @Bean
public Binding binding() {
     System.out.println("队列绑定交换机");
     return BindingBuilder.bind(orderQueue()).to(defaultExchange()).with("order-routing");
}

@Bean
public Queue orderQueue() {
    System.out.println("申明队列");
    *//** 消息默认是持久化的 *//*
    return new Queue("order-queue", true);
}

//发送消息 指定exchange、routing-key
rabbitTemplate.convertAndSend("fade-exhcange", "order-routing",content);

//消息监听
@Component
@RabbitListener(queues = {"order-queue", "goods-queue"})
public class Receiver {
    
    
    @RabbitHandler
    public void processOrderMq(Object obj) {
        System.out.println("收到MQ消息" + obj);
    }
}

最基础的应用如上所示,springboot帮我们做了很多简化的事情,我们只需要做一些配置编写自己的业务代码即可。

延迟队列的实现

spring:
  application: 
    name: fade
  rabbitmq: 
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
@Configuration
@EnableRabbit
public class RabbitConfigure {
    /**延迟(过期)队列 (队列指定ttl)*/
    @Bean(name = "orderProduceQueue")
    public Queue orderProduceQueue() {
        /**队列名、参数*/
        System.out.println("订单检测申明死信队列(队列设置ttl)");
        return QueueBuilder.durable(RabbitMqConstans.Order.PRODUCE_QUEUE_NAME)
                .withArgument("x-dead-letter-exchange", RabbitMqConstans.Order.CONSUME_EXCHANGE_NAME) //指向的是延迟路由名
                .withArgument("x-dead-letter-routing-key", RabbitMqConstans.Order.CONSUME_ROUTING_KEY)//死信携带的routingKey -->指向的是延迟队列(实际消费者队列)
                /**队列的所有消息(若消息为单独设置ttl)ttl均一样*/
                .withArgument("x-message-ttl", RabbitMqConstans.Order.TTL)
                .build();
    }

    //消费者队列
    @Bean(name = "orderDelayConsumeQueue")
    public Queue delayOrderStatusConsumeQueue() {
        System.out.println("申明消费死信路由转发的消息的队列");
        return QueueBuilder.durable(RabbitMqConstans.Order.CONSUME_QUEUE_NAME).build();
    }

    /**信道路由(消费者队列对应的路由)*/
    @Bean(name = "orderDelayConsumeExchange")
    public DirectExchange orderConsumeExchange() {
         return new DirectExchange(RabbitMqConstans.Order.CONSUME_EXCHANGE_NAME, true, false);  
    }

    /**生产者路由*/
    @Bean(name = "orderProduceExchange")
    public DirectExchange orderProduceExchange() {
        return new DirectExchange(RabbitMqConstans.Order.PROUCE_EXCHANGE_NAME, true, false); 
    }

    /**死信队列绑定路由(消费者队列绑定信道路由)*/
    @Bean
    public Binding bindDeadConsumeQueue() {
        return BindingBuilder.bind(delayOrderStatusConsumeQueue()).to(orderConsumeExchange()).with(RabbitMqConstans.Order.CONSUME_ROUTING_KEY);
    }

    /**(生产者对应路由)消息ttl使用队列ttl*/
    @Bean
    public Binding bindDeadQueue() {
        return BindingBuilder.bind(orderProduceQueue()).to(orderProduceExchange()).with(RabbitMqConstans.Order.PROCUE_ROUTING_KEY);
    }
}
//消息发送
System.out.println("订单创建时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        rabbitTemplate.convertAndSend(RabbitMqConstans.Order.PROUCE_EXCHANGE_NAME, RabbitMqConstans.Order.PROCUE_ROUTING_KEY, content);

//消费者
@Component
@RabbitListener(queues = {RabbitMqConstans.Order.CONSUME_QUEUE_NAME})
public class OrderMqConsume {

    @RabbitHandler
    public void processOrderMq(Object obj) {
        System.out.println("收到MQ消息:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println("内容:" + obj);
    }
}


//常量类
public final class RabbitMqConstans {
    
    static class Order {
        /**生产者队列名*/
        public static final String PRODUCE_QUEUE_NAME = "order-produce-queue";
        
        /**生产者交换机名*/
        public static final String PROUCE_EXCHANGE_NAME = "order-produce-exchange";
        
        /**生产者routing-key*/
        public static final String PROCUE_ROUTING_KEY = "order-produce-routing-key";
        
        /**消费者队列名*/
        public static final String CONSUME_QUEUE_NAME = "order-consume-queue";
        
        /**消费者交换机名*/
        public static final String CONSUME_EXCHANGE_NAME = "order-consume-exchange";
        
        /**消费者routing-key*/
        public static final String CONSUME_ROUTING_KEY = "order-consume-routing-key";
        
        /**延迟5秒*/
        public static final int TTL = 5000;
    }
}

测试效果

交换机

队列
从图中可以看到order-produce-queue是一个延迟队列,点进去可以看到队列的特性
队列特性

延迟测试
每条消息都延迟5秒后消费者收到了,测试图是后面补上的。

消息发送,与正常消费通知

    /**实现ConfirmCallback, ReturnCallback接口*/
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("returnedMessage: 消息内容:").append( new String(message.getBody()))
        .append(",replyCode:").append(replyCode).append(",replyText:").append(replyText).append(",exchange:").append(exchange)
        .append(",routingKey:").append(routingKey);
        System.out.println(buffer.toString());
    }
    
    /**
     * 实现ConfirmCallback接口,并在配置文件中申明spring.application.rabbitmq.publisher-confirms=true
     * 就可以知道消息成功发送到MQ服务器没
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            System.out.println("消息发送成功");
        } else {
            System.out.println("消息发送失败:" + cause);
        }
    }

不使用springboot注解,手动声明消息监听

 @Bean("orderListenerContainer")
      public MessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.setQueueNames(RabbitMqConstans.Order.CONSUME_QUEUE_NAME);
        container.setMessageListener(orderListener());
        //为了测试消息确认机制
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        System.out.println("初始化orderListenerContainer....");
        return container;
     }
      
     @Bean("orderQueueListener")
     public ChannelAwareMessageListener orderListener() {
         System.out.println("初始化orderQueueListener.....");
        return new ChannelAwareMessageListener() {

            @Override
            public void onMessage(Message message, Channel channel) throws Exception {
                System.out.println("orderQueueListener....接收到消息" + message.toString());
                System.out.println(message.getMessageProperties().getDeliveryTag());
                int temp = numer.incrementAndGet() % 2;
                if (temp == 0) {
                    System.out.println("ack....");
                    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                }else {
                    System.out.println("nack....");
                    channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
                    
                }
            }
        };
      }

遇到的问题

配置文件配置了手动确认模式,消息在发送后,能延迟收到第一条消息,后续的消息就阻塞不能消费了,主要是RabbitTemplate默认是单例的,没有手动确认消息收到所以一直阻塞大坑。

    @Bean  
    public ConnectionFactory connectionFactory() {  
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory("127.0.0.1", 5672);  
        connectionFactory.setUsername("guest");  
        connectionFactory.setPassword("guest");  
        connectionFactory.setVirtualHost("/");  
        connectionFactory.setPublisherConfirms(true);
        System.out.println("初始化RabbitMQ ConnectionFactory");
        return connectionFactory;  
    }  
  
    @Bean  
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)  
    public RabbitTemplate rabbitTemplate() {  
        RabbitTemplate template = new RabbitTemplate(connectionFactory());
        System.out.println("初始化RabbitTemplate........");
        return template;  
    }

实际应用

说几个在项目中比较典型的应用场景;

  1. 在电商支付中结果校验,订单在创建后假设在20分钟内未支付则系统自动取消订单并释放库存等其他操作。商品定时上架也是此应用

  2. 延迟重试;分布式场景下,A服务调用B服务,假设B服务恰好down了,我们可以设置一个在失败后几分钟内再次重试;下次在上次延迟的时间上在延迟一点时间再次重试,直到超过重试次数上限后再执行其他逻辑。