RabbitMQ 高级特性

目录

1. 重试机制

1.1 重试配置

1.2 配置交换机和队列 

1.3 发送消息

1.4 消费消息

1.5 运行程序

1.6 手动确认

2. TTL

2.1 设置消息的 TTL

2.2 设置队列的 TTL

2.3 两者的区别

3. 死信队列

3.1 死信的概念

3.2 代码示例

4. 延迟队列

4.1 TTL + 死信队列实现

4.2 存在问题

5. 事务

5.1 配置事务管理器

5.2 声明队列 

5.3 生产者

6. 消息分发

6.1 概念

6.2 应用场景

6.2.1 限流

6.2.2 负载均衡


1. 重试机制

在消息传递的过程中,可能会遇到各种问题,如网路故障,服务不可用,资源不足等,这些问题可能导致消息处理失败,为了解决这些问题,RabbitMQ 提供了重试机制,允许消息在处理失败后重新发送,但是如果程序逻辑引起的错误,呢么重试多次也是无用的,可以设置重置次数

1.1 重试配置

spring:
  rabbitmq:
    addresses: amqp://lk:lk@44.34.51.65:5672/order
    listener:
      simple:
        acknowledge-mode: auto
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 5000ms # 初始失败等待时⻓为5秒
          max-attempts: 5 # 最⼤重试次数(包括⾃⾝消费的⼀次)

1.2 配置交换机和队列 

    //重试机制
    public static final String RETRY_QUEUE = "retry.queue";
    public static final String RETRY_EXCHANGE = "retry.exchange";
    //重试机制
    @Bean("retryQueue")
    public Queue retryQueue() {
        return QueueBuilder.durable(Constants.RETRY_QUEUE).build();
    }
    @Bean("retryExchange")
    public DirectExchange retryExchange() {
        return ExchangeBuilder.directExchange(Constants.RETRY_EXCHANGE).build();
    }
    @Bean("retryBinding")
    public Binding retryBinding(@Qualifier("retryExchange") Exchange exchange,@Qualifier("retryQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("retry").noargs();
    }

1.3 发送消息

    @RequestMapping("/retry")
    public String retry() {
        rabbitTemplate.convertAndSend(Constants.RETRY_EXCHANGE,"retry","retry test");
        return "发送成功";
    }

1.4 消费消息

@Component
public class RetryListener {
    @RabbitListener(queues = Constants.RETRY_QUEUE)
    public void ListenerQueue(Message message) throws UnsupportedEncodingException {
        System.out.printf("接收到的消息: %s,delivery: %d",new String(message.getBody(),"UTF-8"),
                message.getMessageProperties().getDeliveryTag());
        //模拟处理失败
        int num = 1 / 0;
        System.out.println("处理完成");
    }
}

1.5 运行程序

可以看到,在重试了 5 次之后抛出了异常,上述的 delivery 一直是 1,没有变化,说明消息已经到达,此时重试时 delivery 是不会变化的

下面对异常进行捕获

@Component
public class RetryListener {
    @RabbitListener(queues = Constants.RETRY_QUEUE)
    public void ListenerQueue(Message message) throws Exception {
        System.out.printf("接收到的消息: %s,delivery: %d \n",new String(message.getBody(),"UTF-8"),
                message.getMessageProperties().getDeliveryTag());
        //模拟处理失败
        try {
            int num = 1 / 0;
            System.out.println("处理完成");
        }catch (Exception e) {
            System.out.println("处理失败");
        }
    }
}

1.6 手动确认

@Component
public class RetryListener {
    @RabbitListener(queues = Constants.RETRY_QUEUE)
    public void ListenerQueue(Message message, Channel channel) throws Exception {
        long delivery = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.printf("接收到的消息: %s,delivery: %d \n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            //模拟处理失败
            int num = 1 / 0;
            System.out.println("处理完成");
            channel.basicAck(delivery,false);
        }catch (Exception e) {
            channel.basicNack(delivery,false,true);
        }
    }
}

可以看到手动确认模式时,重试次数是无效的,因为消息不断地入队列,此时 delivery 是不断递增的

自动确认模式下,RabbitMQ 会在消息被投递给消费者后自动确认消息,如果消费者处理消息时抛出异常,RabbitMQ 会根据配置重试参数自动将消息重新入队,从而实现重试

手动确认模式下,消费者需要手动对消息进行确认,如果消费者在处理消息时发生异常,可以选择不确认消息使消息重新入队

注意:

1)自动确认模式下:程序逻辑异常,多次重试还是失败,消息会被自动确认,从而导致消息丢失

2)手动确认模式下:程序逻辑异常,多次重试消息依然处理失败,无法被确认,就一直是 unacked 的状态,导致消息积压

2. TTL

 TTL(Time to live,过期时间),RabbitMQ 可以消息和队列设置 TTL,当消息到达存活时间之后,还没有被消费,就会被自动删除

2.1 设置消息的 TTL

有两种方法可以设置消息的 TTL

1)设置队列的 TTL,队列中所有消息都有相同的过期时间

2)对消息本身进行单独设置,每条消息的 TTL 可以不同,如果两种方法一起使用,则消息的 TTL 以两者之间较小的数值为准

    //TTL
    public static final String TTL_QUEUE = "ttl.queue";
    public static final String TTL_EXCHANGE = "ttl.exchange";
    //TTL
    @Bean("ttlQueue")
    public Queue ttlQueue() {
        return QueueBuilder.durable(Constants.TTL_QUEUE).build();
    }
    @Bean("ttlExchange")
    public DirectExchange ttlExchange() {
        return ExchangeBuilder.directExchange(Constants.TTL_EXCHANGE).build();
    }
    @Bean("ttlBinding")
    public Binding ttlBinding(@Qualifier("ttlExchange") Exchange exchange,@Qualifier("ttlQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("ttl").noargs();
    }
    @RequestMapping("/ttl")
    public String ttl() {
        String ttlTime = "10000"; //10s
        rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE,"ttl","ttl test",message -> {
            message.getMessageProperties().setExpiration(ttlTime);
            return message;
        });
        return "发送成功";
    }

运行程序,观察结果

经过 10s 之后消息被删除,如果不设置 TTL,则表示消息不会过期,如果设置 TTL 为0,则表示除非直接将此消息投递到消费者,否则该消息会被立刻丢弃

2.2 设置队列的 TTL

设置队列 TTL 的方法是在创建队列时,加入  x-message-ttl 参数实现的

配置队列和绑定关系

public static final String TTL_QUEUE2 = "ttl.queue2";
    @Bean("ttlQueue2")
    public Queue ttlQueue2() {
        return QueueBuilder.durable(Constants.TTL_QUEUE2).ttl(10000).build();
    }
    @Bean("ttlBinding2")
    public Binding ttlBinding2(@Qualifier("ttlExchange") Exchange exchange,@Qualifier("ttlQueue2") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("ttl2").noargs();
    }
@RequestMapping("/ttl")
    public String ttl() {
        //发送不带 ttl 的消息
        rabbitTemplate.convertAndSend(Constants.TTL_EXCHANGE,"ttl2","ttl test");
        return "发送成功";
    }

 10s 之后刷新页面,发现消息被删除

2.3 两者的区别

设置队列的 TTL ,一旦消息过期,就会从队列中删除

设置消息的 TTL,即使消息过期,也不会立刻从队列中删除,而是在即将投递到消费者之前进行判定的

设置队列的 TTL,队列中已过期的消息肯定在队列头部,RabbitMQ 只要定期从队头开始扫描是否有过期的消息即可

而设置消息的 TTL,由于每条消息的过期时间不同,如果要删除所有过期消息需要扫描整个队列,如果等到该消息及激昂被消费时在判定是否过期,如果过期再进行删除即可

3. 死信队列

3.1 死信的概念

死信就是因为各种原因无法被消费的信息,当消息再一个队列中变成死信之后,它能被重新发送到另一个交换机中,这个交换机就是 DLX,绑定 DLX 的队列,称为死信队列

 消息变成死信一般是因为以下几种情况:

1)消息被拒绝(Basic.Reject / Basic.Nack),并且设置参数为 false

2)消息过期

3)队列达到最大长度

3.2 代码示例

1)声明交换机和队列

    //正常队列和交换机
    public static final String NORMAL_QUEUE = "normal.queue";
    public static final String NORMAL_EXCHANGE = "normal.exchange";
    //死信队列和交换机
    public static final String DLX_QUEUE = "dLX.queue";
    public static final String DLX_EXCHANGE = "dLX.exchange";
    //正常
    @Bean("normalQueue")
    public Queue normalQueue() {
        return QueueBuilder.durable(Constants.NORMAL_QUEUE).build();
    }
    @Bean("normalExchange")
    public DirectExchange normalExchange() {
        return ExchangeBuilder.directExchange(Constants.NORMAL_EXCHANGE).build();
    }
    @Bean("normalBinding")
    public Binding normalBinding(@Qualifier("normalExchange") Exchange exchange,@Qualifier("normalQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("normal").noargs();
    }
    //死信
    @Bean("dlQueue")
    public Queue dlQueue() {
        return QueueBuilder.durable(Constants.DLX_QUEUE).build();
    }
    @Bean("dlExchange")
    public DirectExchange dlExchange() {
        return ExchangeBuilder.directExchange(Constants.DLX_EXCHANGE).build();
    }
    @Bean("dlBinding")
    public Binding dlBinding(@Qualifier("dlExchange") Exchange exchange,@Qualifier("dlQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with("dl").noargs();
    }

2)正常队列绑定死信交换机
当这个队列中存在死信时,RabbitMQ 会自动地把这个消息重新发布到设置地 DLX 上,进而被路由到另一个队列,即死信队列,可以监听这个死信队列中的消息进行相应的处理

    @Bean("normalQueue")
    public Queue normalQueue() {
        return QueueBuilder.durable(Constants.NORMAL_QUEUE)
                .deadLetterExchange(Constants.DLX_EXCHANGE)
                .deadLetterRoutingKey("dl")
                .ttl(10000)
                .maxLength(10L)
                .build();
    }

3)发送消息

    @RequestMapping("/dl")
    public String dl() {
        rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test");
        return "发送成功";
    }

4)运行程序,观察结果

 可以看到过了 10s 之后消息进入了死信队列

接下来测试 20 条数据

    @RequestMapping("/dl")
    public String dl() {
        for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test" + i);
        }
        return "发送成功";
    }

由于设置了队列的长度为 10,发了 20 条数据,所以发送 20 条消息,就会有 10 条消息立刻进入死信队列,当消息过期之后,正常队列的 10 条消息也会进入死信队列

5)测试消息拒收

@Component
public class DlListener {
    @RabbitListener(queues = Constants.NORMAL_QUEUE)
    public void ListenerQueue(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.printf("接收到消息: %s,deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            int num = 1 / 0;
            channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            //设置消息不会重新发送,直接丢弃进入死信队列
            channel.basicNack(deliveryTag,false,false);
        }
    }
    @RabbitListener(queues = Constants.DLX_QUEUE)
    public void ListenerDLXQueue(Message message,Channel channel) throws Exception{
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.printf("死信队列接收到的消息: %s,deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
                message.getMessageProperties().getDeliveryTag());
    }
}

4. 延迟队列

延迟队列(Delayed Queue),即消息被发送以后并不像让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费

RabbitMQ 本身没有支持延迟队列的功能,但是可以通过 TTL + 死信队列的方式组合模拟出延迟队列的功能

假设一个应⽤中需要将每条消息都设置为10秒的延迟,⽣产者通过 normal_exchange 这个交换器将发送的消息存储在 normal_queue 这个队列中,消费者订阅的并非是 normal_queue 这个队列,而是 dlx_queue 这个队列,当消息从 normal_queue 这个队列中过期之后被存入 dlx_queue 这个
队列中,消费者恰好消费了延迟 10s 的这条消息

所以在死信队列章节的就是延迟队列的使用

4.1 TTL + 死信队列实现

继续用死信队列的代码

生产者发送两条消息,第一条 10s 过期,第二条 20s 过期

    @RequestMapping("/dl")
    public String dl() {
        rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test 10s", message -> {
            message.getMessageProperties().setExpiration("10000");
            return message;
        });
        rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test 20s",message -> {
            message.getMessageProperties().setExpiration("20000");
            return message;
        });
        System.out.printf("%tc 消息发送成功 \n",new Date());
        return "发送成功";
    }
    @RabbitListener(queues = Constants.DLX_QUEUE)
    public void ListenerDLXQueue(Message message,Channel channel) throws Exception{
        System.out.printf("%tc 死信队列接收到的消息: %s,deliveryTag: %d \n",new Date(),new String(message.getBody(),"UTF-8"),
                message.getMessageProperties().getDeliveryTag());
    }

可以看到两条消息按照过期时间一次进入了死信队列

4.2 存在问题

把生产消息的顺序修改以下,先发送 20s 的消息,再发送 10s 的消息

    @RequestMapping("/dl")
    public String dl() {
        rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test 20s",message -> {
            message.getMessageProperties().setExpiration("20000");
            return message;
        });
        rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test 10s", message -> {
            message.getMessageProperties().setExpiration("10000");
            return message;
        });
        System.out.printf("%tc 消息发送成功 \n",new Date());
        return "发送成功";
    }

这时候发现 10s 过期的消息也是在 20s 后才进入死信队列中,这是因为,RabbitMQ 只会检查队首消息是否过期,如果过期则丢到死信队列中,20s 的消息是在队首,如果第一个消息的延时时间很长,第二个消息的延时很短,那么第二个消息并不会优先执行,因此 TTL + 死信队列实现延迟队列的时候,需要确保每个任务的延迟时间是一致的

5. 事务

RabbitMQ 是基于 AMQP 协议实现的,该协议实现了事务机制,因此 RabbitMQ 也支持事务机制,Spring AMQP 也提供了事务的相关操作,RabbitMQ 事务允许开发者确保消息的发送和接收是原子的,要么全部成功,要么全部失败

5.1 配置事务管理器

@Configuration
public class TransConfig {
    @Bean
    public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory) {
        return new RabbitTransactionManager(connectionFactory);
    }
    @Bean("transRabbitTemplate")
    public RabbitTemplate transRabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        //开启事务
        rabbitTemplate.setChannelTransacted(true);
        return rabbitTemplate;
    }
}

5.2 声明队列 

@Configuration
public class RabbitMQConfig {
    @Bean("transQueue")
    public Queue transQueue() {
        return QueueBuilder.durable(Constants.TRANS_QUEUE).build();
    }
}

5.3 生产者

@RequestMapping("/producer")
@RestController
public class ProducerController {
    @Autowired
    private RabbitTemplate transRabbitTemplate;
    @Transactional
    @RequestMapping("/trans")
    public String trans() {
        transRabbitTemplate.convertAndSend("", Constants.TRANS_QUEUE,"trans test1");
        int num = 1 / 0;
        transRabbitTemplate.convertAndSend("", Constants.TRANS_QUEUE,"trans test2");
        return "发送成功";
    }
}

不添加 @Transactional,会发现消息 1 发送成功

添加 @Transactional,消息 1 和 2 全部发送失败

6. 消息分发

6.1 概念

RabbitMQ 队列有多个消费者时,队列会把收到的消息分派给不同的消费者,每条消息只会发送给订阅列表的一个消费者,如果现在负载加重,那么只需要创建更多的消费者来消费处理消息即可

默认情况下,RabbitMQ 是以轮询的方式分发的,如果某些消费者消费速度慢,而某些消费者消费速度快,就可能导致某些消费者消息积压,某些消费者空闲,从而导致整体的吞吐量下降

此时就可以使用 channel.basicQos(int prefetchCount) 方法来限制当前信道上的消费者所能保持最大未确认消息的数量

例如消费端调用了 channel.basicQos(5),RabbitMQ 会为该消费者计数,发送一条消息计数 +1,消费一条消息计数 -1,当达到了设定的上限,RabbitMQ 就不会在向它发送消息了,直到消费者确认了某条消息,类似于 TCP 中的滑动窗口,prefetchCount 设置为0时表示没有上限

6.2 应用场景

消息分发的常见应用场景如下:

1)限流

2)非公平分发

6.2.1 限流

例如订单系统每秒最多处理 5000 请求,在正常情况下,订单系统可以满足需求,但是当请求瞬间增多,每秒 1w 个请求时,这些请求全部发送 MQ 订单系统中,会把系统压垮

RabbitMQ 提供了限流机制,可以控制消费端一次只拉取 N 个请求,通过设置 prefetchCount 参数,同时必须要设置消息应答方式为手动应答

prefetchCount:控制消费者从队列中预取(prefetch)消息的数量,以此来实现流控制和负载均衡

代码示例:

1)配置 prefetch,设置应答方式为手动应答

listener:
  simple:
    acknowledge-mode: manual
    prefetch: 5

2)配置交换机和队列

    //限流
    public static final String QOS_QUEUE = "qos.queue";
    public static final String QOS_EXCHANGE = "qos.exchange";
    //限流
    @Bean("qosQueue")
    public Queue qosQueue() {
        return QueueBuilder.durable(Constants.QOS_QUEUE).build();
    }
    @Bean("qosExchange")
    public DirectExchange qosExchange() {
        return ExchangeBuilder.directExchange(Constants.QOS_EXCHANGE).build();
    }
    @Bean("qosBinding")
    public Binding qosBinding(@Qualifier("qosExchange") Exchange exchange, @Qualifier("qosQueue") Queue queue) {
       return BindingBuilder.bind(queue).to(exchange).with("qos").noargs(); 
    }

3)发送消息,一次发送 20 条消息

    @RequestMapping("/qos")
    public String qos() {
        for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend(Constants.QOS_EXCHANGE,"qos","qos test" + i);
        }
        return "发送成功";
    }

4)消费者监听

@Component
public class QosListener {
    @RabbitListener(queues = Constants.QOS_QUEUE)
    public void ListenerQueue(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.printf("接收到的消息: %s, deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            //消费者不进行手动确认
            //channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            channel.basicNack(deliveryTag,false,true);
        }

    }
}

运行程序,观察结果

可以看到,ready,也就是待发送 15 条,未确认 5 条(因为代码没有手动 ack)

将 prefetch: 5 注掉,再次观察结果

6.2.2 负载均衡

也可以使用此配置来实现"负载均衡"

例如,在有两个消费者的情况下,一个消费者处理任务非常快,另一个非常慢,就会造成一个消费者很忙,另一个很闲,此时可以设置 prefetch=1 的方式,告诉 RabbitMQ 一次只给一个消费者一条消息,也就是说,在处理并确定一条消息之前,不要向该消费者发送新消息,相反,将任务派发给下一个不忙的消费者

代码示例:

1)配置 prefetch,设置应答方式为手动应答

listener:
  simple:
    acknowledge-mode: manual
    prefetch: 1

2)启动两个消费者

@Component
public class QosListener {
    @RabbitListener(queues = Constants.QOS_QUEUE)
    public void ListenerQueue(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.printf("消费者1接收到的消息: %s, deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            channel.basicNack(deliveryTag,false,true);
        }
    }
    @RabbitListener(queues = Constants.QOS_QUEUE)
    public void ListenerQueue2(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.printf("消费者2接收到的消息: %s, deliveryTag: %d \n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            //模拟处理流程慢
            Thread.sleep(100);
            channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            channel.basicNack(deliveryTag,false,true);
        }
    }
}

运行程序,观察结果

可以明显的看到消费者 2 处理的消息比较少

上述 deliveryTag 有重复是因为两个消费者使用的是不同的 Channel,每个 Channel 上的deliveryTag 都是独立计数的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值