Rabbit MQ之消息延时消费与重复消费

1、关于消息延时

1.1、消息延时的定义

Rabbit MQ并没有直接支持消息延时的功能,但是可以通过设置队列(消息)的过期时间(TTL)和死信交换机(DLX)来实现消息这一个功能,估计没听过TTL和DLX的童鞋是不是一脸懵?这是个啥东西?哈哈…那现在就来看一下TTL和DXL的定义吧。

TTL是Time to Live 的简称,就是过期时间的意思,RabbitMQ 可以对消息和队列设置TTL。

  • 对队列设置过期时间,那么在这个队列的所有消息都有相同的过期时间了。
  • 单独对每条消息设置过期时间,那么每条消息的过期时间可以不同。

延时交换机和延时队列设置如下,SpringBoot + RabbitMQ的代码如下:

@Configuration
public class RabbitMqProducerConfig {
    //延时交换机
    public static final String TTL_EXCHANGE = "ttl.exchange";
    //延时队列
    public static final String TTL_QUEUE = "ttl_queue";
    //延时交换机的routing_key
    public static final String TTL_ROUTING_KEY = "ttl.routingkey";

    /**
     * 设置一个延时队列
     *
     * @return
     */
    @Bean
    public Queue ttlQueue() {
        Map<String, Object> params = new HashMap<>();
        //设置消息的过期时间,单位:毫秒
        params.put("x-message-ttl", 10000);
        //设置延时交换机
        params.put("x-ttl.exchange", TTL_EXCHANGE);
        //设置延时交换机的路由键
        params.put("x-ttl.routingkey", TTL_ROUTING_KEY);
        return QueueBuilder.durable(TTL_QUEUE).withArguments(params).build();
    }

    /**
     * 设置一个延时交换机
     */
    @Bean
    public TopicExchange ttlExchange() {
        return new TopicExchange(TTL_EXCHANGE);
    }

    /**
     * 死信队列绑定死信交换机
     */
    @Bean
    public Binding ttlqueueBindExchange() {
        return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with(TTL_ROUTING_KEY);
    }
}

那么队列(消息)过期后,会怎么样? 当然是Rabbit MQ回收啦,不过这样不就达不到消息延时消费的这个目的了吗?消息都被回收了,消费者怎么消费呢?所以Rabbit MQ出个东西,叫做死信交换机(DLX),那么DLX是怎么定义的呢?

DLX是Dead-Letter-Exchange的简称,就是死信交换机的意思。死信交换机和普通的交换机没有什么区别,只是当
消息在一个队列中变成死信之后,它能被重新被发送到另一个交换器中,而这个交换机就是死信交换机。那么和死信交换机绑定的队列叫啥呢?不用猜肯定就叫死信队列了。

那么什么样的消息才会变成死信呢?如下三种情况出现,消息就会变成死信:

  • 消息被拒绝,并且设置 requeue 参数为 false
  • 消息过期
  • 队列达到最大长度

死信交换机和死信队列设置如下,SpringBoot + RabbitMQ的代码:

@Configuration
public class RabbitMqProducerConfig {
   //死信交换机
    public static final String TTL_DLK_EXCHANGE = "ttl.dlk.exchange";
    //死信交换机的routing_key
    public static final String TTL_DLK_ROUTING_KEY = "ttl.dlk.routingkey";
    //死信队列
    public static final String TTL_DLK_QUEUE = "ttl_dlk_queue";

    /**
     * 申明一个死信队列
     *
     * @return
     */
    @Bean
    public Queue dlkQueue() {
        Map<String, Object> params = new HashMap<>();
        //设置死信交换机
        params.put("x-dead-letter-exchange", TTL_DLK_EXCHANGE);
        //设置死信交换机的路由键
        params.put("x-dead-letter-routing-key", TTL_DLK_ROUTING_KEY);
        //durable 表示持久化
        return QueueBuilder.durable(TTL_DLK_QUEUE).withArguments(params).build();
    }

    /**
     * 申明一个topic类型的死信交换机
     *
     * @return
     */
    @Bean
    public TopicExchange dlkExchange() {
        return new TopicExchange(TTL_DLK_EXCHANGE);
    }

    /**
     * 死信队列绑定死信交换机
     */
    @Bean
    public Binding dlkqueueBindExchange() {
        return BindingBuilder.bind(dlkQueue()).to(dlkExchange()).with(TTL_DLK_ROUTING_KEY);
    }
}

通过上面的内容,相信童鞋们已经了解了什么是TTL和DLX了,那么就很容易通过TTL和DLX来设置消息的延时消费了。

其实延时消费就是指消费者在指定等待多长时间后才能拿到消息进行消费,那么我们就可以将死信队列看做是一个延时队列,因为我们可以设置消息的过期时间,比如我们将消息的过期时间设置为10s,且不让它匹配任何消费者,那么10s后,这个消息就会变成死信,通过DLX的定义可以知道,死信会先重发到死信交换机,然后死信交换机会将消息发送到和它绑定的死信队列当中,死信队列会将这个消息发送给和它匹配的消费者进行消费,那么这样就相当于消费者延迟了10s才把这个消息进行消费了。

至此,TTL和DLX就完成了整个消息的延时设置了。

1.2、延时队列的使用场景
1.2.1、实际工作当中
  • 领导叫你设计一个功能:我们允许客户延时30分钟进行支付,如果过期30分钟了,客户还没有支付,就把这笔订单取消。学习了上面的知识,是不是很容易解决了?
  • 再比如,用户希望设置某个在时间点对某个商品进行自动下单,下完单后再提醒客户进行付款?是不是也可以用延时消息做呢?
1.2.1、面试
  • 你知道Rabbit MQ中的TTL和DLX吗?
    这个就很easy啦,把上面的定义说一遍就好了。
  • Rabbit MQ怎么样设计一个延时队列呢?
    理解了TTL和DLX的概念,这个问题也好回答
  • 消息会不会出现重复消费的情况呢?怎么解决?
    当然会啊,很经典的一个场景,比如当消费者消费完一条消息后,想给Rabbit MQ服务器发送消费确认信息,但是此时,由于网络原因或者服务器宕机,导致服务器并没有接收到消费者的消费确认信息,那么Rabbit MQ服务器会将次消息再转发给其它消费者,这样就出现了重复消费的问题。那么如何解决呢?请看下文。
2、解决消息的重复消费问题

其实思路很简单,现在就是要解决消息的重复消费问题,那么在我们进行消费之前,查询一下这条消息是否已经消费过,如果已经消费过了,那么这次就不消费了,如果没有消费,那么就继续消费,这样不就解决重复消费的问题了吗?那么专业术语怎么回答呢?

  • 首先需要查询这条消息,那么就需要确定这条消息是唯一的,需要保证消息的唯一性。
  • 其次根据消息是否已经被消费过了,来确定是否需要消费。这就是说,对于消费者来说,无论请求这个消息多少次,我请求的最终结果都是正确的,不会因为请求次数次数而改变,而这个呢,专业术语就叫做消息的幂等性!。

专业术语总结如下:要解决消息的重复消费问题,首先要保证消息的唯一性,其次要保证消息的幂等性!

幂等性定义:一次和多次请求某一个资源对于资源本身应该具有同样的结果,也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。在编程中,就是一个请求,不管其请求多少次,其结果都不会改变

2.1、消息的唯一性

这个需要根据实际情况来定,如果是订单系统的话,那么订单号是可以作为标志消息的唯一性标志。或者组装一个消息ID作为订单的唯一标志都是可以的。订单的唯一标志加订单的状态足可以保证消息的幂等性。

2.1、使用场景

下面列举两个例子,帮助大家理解:
<1>假如在一个系统中,你消费了一条消息之后,就会往数据库插入一条数据,那么在这个消息第二次到来时,你可以根据这个消息的唯一标志查询数据库,该消息是否已经存在,如果不存在,则进行消费插入数据即可。如果存在数据,则证明已经消费,那么直接丢弃这条消息即可。
当然你也可以不查询数据库,直接往数据表插入数据,但是如果你需要保证消息不被重复,就是数据库不能存在两条一样的消息。因此你需要将消息的唯一标志做为数据表的唯一键。
<2>假如在一个订单系统中,已经存在了一条订单在数据表中,订单状态为未支付,此时来了一个消息,要更改这条订单的状态,将未支付的订单该成已支付,显然需要根据订单号和订单状态去更改这条订单数据,SQL语句如下:

update  order_table  
      set status = '已支付'
      where  order_id =  #{order_id}  
      and status =  ‘未支付’

如上面语句所示,当第二条重复消息来的时候,由于此时订单已经被修改成已支付了,显然条件不匹配,消息不会被执行。所以第二条消息就不会被重复消费了。

                                             全篇终,致敬!

欢迎各位关注我的JAVAERS公众号,陪你一起学习,一起成长,一起分享JAVA路上的诗和远方。在公众号里面都是JAVA这个世界的朋友,公众号每天会有技术类文章,面经干货,也有进阶架构的电子书籍,如Spring实战、SpringBoot实战、高性能MySQL、深入理解JVM、RabbitMQ实战、Redis设计与实现等等一些高质量书籍,关注公众号即可领取哦。
在这里插入图片描述

发布了11 篇原创文章 · 获赞 92 · 访问量 6485

猜你喜欢

转载自blog.csdn.net/qq_36526036/article/details/105129174