RabbitMQ 的高级特性浅尝(confirm,return,ACK,prefetch,TTL,DLX,delay)

RabbitMQ 的高级特性浅尝

有需要的可以查看上一篇 RabbitMQ 基础内容
在这里插入图片描述
在这里插入图片描述

消息的可靠投递

作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我哦哦们提供了两种方式用来控制消息的投递可靠性模式:

  • confirm 确认模式
  • return 退回模式

rabbitmq 整个消息投递的路径为:producer --> rabbitmq broker --> exchange --> queue --> consumer

  • 消息从produce 到 exchange 成功则会返回一个confirmCallback 。
  • 消息从 exchange --> queue投递失败则会返回一个returnCallback。

我们可以利用这两个callback 控制消息的可靠性投递。

  1. 配置中开启确认模式
spring:
  rabbitmq:
    addresses: 127.0.0.1 # mq ip地址
    host: 5672 # 默认为5672
    username: guest # 默认为guest
    password: guest # 默认为guest
    virtual-host: /mohen # 默认为/
    
    publisher-confirm-type: correlated # confirm 模式
    publisher-returns: true # return 模式
    
# NONE值是禁用发布确认模式,是默认值
# CORRELATED值是发布消息成功到交换器后会触发回调方法,如1示例
# SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
  1. 在rabbitTemplate 定义回调函数
// ConfirmCallBack
@Test
void confirmQueue() {
    /**
    correlationData: 相关配置信息
    ack: 交换机是否成功收到消息。true 成功,false 失败
    cause: 失败原因
     */
    rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
        if (ack) {
            System.out.println("消息发送成功!");
        }else {
            System.out.println("消息发送失败!");
        }
    });
    rabbitTemplate.convertAndSend(RabbitConfig.DIRECT_CONFIRM_EXCHANGE_NAME02, "confirm", "confirm");
}

// ReturnsCallback
@Test
void returnQueue() {
    rabbitTemplate.setReturnsCallback(returned -> {
        System.out.println("消息发送失败");
        System.out.println("Message: " + returned.getMessage());
        System.out.println("ReplyCode: " + returned.getReplyCode());
        System.out.println("ReplyText: " + returned.getReplyText());
        System.out.println("Exchange: " + returned.getExchange());
        System.out.println("RoutingKey: " + returned.getRoutingKey());
    });
    rabbitTemplate.convertAndSend(RabbitConfig.DIRECT_CONFIRM_EXCHANGE_NAME02, "confirm", "confirm");
}

Consumer Ack

ack 指Acknowlege 确认,表示消费端收到消息后的确认方式。

有三种模式:

  • 自动确认:acknowledge:
  • 手动确认:acknowledge:
  • 根据异常情况确认:acknowledge:

自动确认是指:当消息 一旦被Consumer 接收到,就自动确认,并将相应的message 从队列的缓存中移除。但是实际业务中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。

手动确认:自己在业务处理成功后,手动调用channel.basicAck() 手动签收,如果出现异常则调用channel.basicNack() 方法,让其自动重新发送消息。

  1. 在消费端配置中开启手动模式
spring:
  rabbitmq:
    addresses: 127.0.0.1 # mq ip地址
    host: 5672 # 默认为5672
    username: guest # 默认为guest
    password: guest # 默认为guest
    virtual-host: /mohen # 默认为/
    listener:
      simple:
        acknowledge-mode: manual # 开启手动确认
  1. 生产端发送消息
  2. 消费端开启监听
@Component
public class AckListener {

    @RabbitListener(queues = "confirm_queue")
    public void ackListener(Message message, Channel channel) throws InterruptedException {
        Thread.sleep(2000);
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));
            //2.处理业务逻辑
            System.out.println("处理业务逻辑!");
            //3.若没有异常则进行消息签收
            int i = 3 / 0;
            /**
             * basicAck(long deliveryTag, boolean multiple)
             * 参数:
             * deliveryTag:收到的消息的标签
             * multiple:是否签收所有消息
             */
            channel.basicAck(deliveryTag, false);
            //模拟异常
            System.out.println("已签收消息!");
        } catch (Exception e) {
            //出现异常,拒收消息
            /**
             * basicNack(long deliveryTag, boolean multiple, boolean requeue)
             * 参数:
             * deliveryTag:收到的消息的标签
             * multiple:是否签收所有消息
             * requeue:重回队列。若设置为true,则消息会重新回到队列,broker会重新发送该消息给消费端
             */
            try {
                System.out.println("已拒收消息!");
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

消费端限流

配置中开启限流,前提条件保证开启手动确认。其他地方同上即可

spring:
  rabbitmq:
#    addresses: 127.0.0.1 # mq 地址
#    host: 5672
#    username: guest # 默认为guest
#    password: guest # 默认为guest
    virtual-host: /mohen # 默认为/
    publisher-confirm-type: correlated
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual # 开启手动确认
        prefetch: 2 # 开启限流,每次手动签收后,再处理下一批

TTL

ttl:Time to Live 存活时间,当消息到达存活时间后,还没有被消费会被自动清除。

  • RabbitMQ 可以对单个消息设置过期时间,也可以对整个队列设置过期时间。

  • 创建队列时,通过x-message-ttl参数统一设置队列过期时间,单位:ms。

  • 消息的过期时间通过expiration 参数设置,单位:ms。

  • 如果两者都设置了,以时间短的为准,队列过期后,会将队列中所有的消息全部移除。

  • 消息过期后,不会立即将其移除,只有在队列顶端时,才会判断其是否过期。

public static final String TTL_QUEUE_NAME = "ttl_queue";

// 队列过期时间设置
@Bean(TTL_QUEUE_NAME)
public Queue ttlQueue() {
    // x-message-ttl: 最大存活时间
    return QueueBuilder.durable(TTL_QUEUE_NAME).withArgument("x-message-ttl",20000).build();
}

// 消息过期时间设置
@Test
void ttlQueue() {
    // 1. 统一设置过期时间
 rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE_NAME, "ttl.chsj", "ttl-ty");
    // 2. 单独设置过期时间
 rabbitTemplate.convertAndSend(RabbitConfig.TOPIC_EXCHANGE_NAME, "ttl.chsj", "ttl-dd", message -> {
        // 1.设置message 的信息过期时间
        message.getMessageProperties().setExpiration("5000");
        // 2. 返回该消息
        return message;
    });
}

死信队列

dlx: Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另外一个交换机,这个交换机就是dlx,与普通消息没有太大区别。

在这里插入图片描述

消息成为死信的三种情况:
  • 队列消息长度到达限制

  • 消费者拒收,basicNack/basicReject 并且不把消息重新放入原目标队列

    有一种场景需要注意下:消费者设置了自动 ACK,当重复投递次数达到了设置的最大 retry 次数之后,消息也会投递到死信队列,但是内部的原理还是调用了 basicNack()basicReject()(这种情况我没写例子)
    这个帖子写的比较详细: 死信队列,点击查看

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

队列绑定死信交换机:

给正常队列设置参数: x-dead-letter-exchange 死信交换机名称 和 x-dead-letter-routing-key 发送给死信交换机的路由键

分别来展示这几种情况,首先定义死信队列和正常队列

// 死信交换机
public static final String DLX_EXCHANGE_NAME = "dlx_exchange";
// 正常
public static final String UNDLX_EXCHANGE_NAME = "undlx_exchange";

// 死信
public static final String DLX_QUEUE_NAME = "dlx_queue";
// 正常
public static final String UNDLX_QUEUE_NAME = "undlx_queue";
public static final String UNDLX_QUEUE_NAME02 = "undlx_queue02";


// 定义交换机
@Bean(DLX_EXCHANGE_NAME)
public Exchange dlxExchange() {
    return ExchangeBuilder.topicExchange(DLX_EXCHANGE_NAME).durable(true).build();
}
@Bean(UNDLX_EXCHANGE_NAME)
public Exchange undlxExchange() {
    return ExchangeBuilder.topicExchange(UNDLX_EXCHANGE_NAME).durable(true).build()
}

// 定义队列
@Bean(DLX_QUEUE_NAME)
public Queue dlxQueue() {
    return QueueBuilder.durable(DLX_QUEUE_NAME).build();
}
@Bean(UNDLX_QUEUE_NAME)
public Queue undlxQueue() {
    Map<String, Object> map = new HashMap<>();
    map.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
    map.put("x-dead-letter-routing-key", "dlx.hhh");
    return QueueBuilder.durable(UNDLX_QUEUE_NAME).withArguments(map).build();
}
@Bean(UNDLX_QUEUE_NAME02)
public Queue undlx02Queue() {
    Map<String, Object> map = new HashMap<>();
    map.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
    map.put("x-dead-letter-routing-key", "dlx.hhh");
    //x-max-length: 队列最大长度
    map.put("x-max-length", 2);
    return QueueBuilder.durable(UNDLX_QUEUE_NAME02).withArguments(map).build();
}

//绑定交换机与队列
@Bean
public Binding bindQueueExchange09(@Qualifier(DLX_QUEUE_NAME) Queue queue, @Qualifier(DLX_EXCHANGE_NAME) Exchange exchange) {
    return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
}
@Bean
public Binding bindQueueExchange10(@Qualifier(UNDLX_QUEUE_NAME) Queue queue, @Qualifier(UNDLX_EXCHANGE_NAME) Exchange exchange) {
    return BindingBuilder.bind(queue).to(exchange).with("undlx.#").noargs();
}
@Bean
public Binding bindQueueExchange11(@Qualifier(UNDLX_QUEUE_NAME02) Queue queue, @Qualifier(UNDLX_EXCHANGE_NAME) Exchange exchange) {
    return BindingBuilder.bind(queue).to(exchange).with("undlx02.#").noargs();
}

生产端发送消息,代码我写在一起了,可以分别放开注释进行测试

@Test
void dlxQueue() {
    // 1. 消费端拒收
    // rabbitTemplate.convertAndSend(RabbitConfig.UNDLX_EXCHANGE_NAME, "undlx.chsj", "undlx-nack");
    // 2. 队列长度限制
    /*for (int i = 5; i > 0; i--) {
  rabbitTemplate.convertAndSend(RabbitConfig.UNDLX_EXCHANGE_NAME, "undlx02.chsj", "undlx-maxLength");
}*/
    // 3. 过期时间,测试这个时,消费端不启动,在管理控制台可以看到消息从正常队列转到死信队列中
  /*rabbitTemplate.convertAndSend(RabbitConfig.UNDLX_EXCHANGE_NAME, "undlx.chsj", "undlx-ttl", message -> {
        // 1. 设置message 的信息过期时间
        message.getMessageProperties().setExpiration("5000");
        // 2. 返回该消息
        return message;
    });*/
}

消费端也写在一起了

@Component
public class DlxListener {

    // @RabbitListener(queues = "undlx_queue")
    @RabbitListener(queues = "undlx_queue02") // 用于测试长度限制
    public void undlxListener(Message message, Channel channel) {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            Thread.sleep(2000);
            System.out.println("正常-接收: " + new String(message.getBody()));
            channel.basicAck(deliveryTag, true);
            // System.out.println("正常-拒收,并不重回队列: " + new String(message.getBody()));
            // channel.basicNack(deliveryTag, true, false); // 用于测试拒收
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    @RabbitListener(queues = "dlx_queue")
    public void dlxListener(Message message, Channel channel) {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.println("死信-接收: " + new String(message.getBody()));
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            try {
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
常见需求:下单后,30 分钟未付款,订单自动取消。
实现方式:1. 定时器 2. 延迟队列

在这里插入图片描述
但是RabbitMQ 中并没有直接提供延迟队列的功能,不过可以使用TTL+DLX组合的方式来实现延迟队列的效果.
在这里插入图片描述
模拟上述需求,代码与进入死信队列的第三种情况类似,这里我也写一下吧

// 配置信息
public static final String DLX_EXCHANGE_NAME = "dlx_exchange";
public static final String DELAY_EXCHANGE_NAME = "delay_exchange";
public static final String DLX_QUEUE_NAME = "dlx_queue";
public static final String DELAY_QUEUE_NAME = "delay_queue";

@Bean(DLX_EXCHANGE_NAME)
public Exchange dlxExchange() {
    return ExchangeBuilder.topicExchange(DLX_EXCHANGE_NAME).durable(true).build();
}
@Bean(DELAY_EXCHANGE_NAME)
public Exchange delayExchange() {
    return ExchangeBuilder.topicExchange(DELAY_EXCHANGE_NAME).durable(true).build();
}

@Bean(DLX_QUEUE_NAME)
public Queue dlxQueue() {
    return QueueBuilder.durable(DLX_QUEUE_NAME).build();
}
@Bean(DELAY_QUEUE_NAME)
public Queue delayQueue() {
    Map<String, Object> map = new HashMap<>();
    map.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);
    map.put("x-dead-letter-routing-key", "dlx.delay");
    map.put("x-message-ttl", 10000); // 10s
    return QueueBuilder.durable(DELAY_QUEUE_NAME).withArguments(map).build();
}

@Bean
public Binding bindQueueExchange09(@Qualifier(DLX_QUEUE_NAME) Queue queue, @Qualifier(DLX_EXCHANGE_NAME) Exchange exchange) {
    return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
}
@Bean
public Binding bindQueueExchange12(@Qualifier(DELAY_QUEUE_NAME) Queue queue, @Qualifier(DELAY_EXCHANGE_NAME) Exchange exchange) {
    return BindingBuilder.bind(queue).to(exchange).with("#.delay").noargs();
}


// 模拟调用订单系统
@Test
void delayQueue() {
    rabbitTemplate.convertAndSend(RabbitConfig.DELAY_EXCHANGE_NAME, "order.delay", "order-delay");
}

//消费端接收
@Component
public class DalayListener {
    // 实现延迟队列效果,需要监听的是死信队列
    @RabbitListener(queues = "dlx_queue")
    public void dlxListener(Message message, Channel channel) {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.println("死信-接收: " + new String(message.getBody()) + "开始实现业务逻辑...");
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            try {
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

日志与监控

日志

在Windows 上的日志文件在 C:\Users\用户名\AppData\Roaming\RabbitMQ\log/rabbit@主机名.log

Linux:/var/log/rabbitmq/rabbit@主机名.log

日志包含了Eralng 的版本号,RabbitMQ 的版本号、服务节点名称,cookie的hash值等等信息,有利于异常情况发生后排查问题。

管理控制台

在这里插入图片描述
可以监控rabbitmq 使用的连接,信道,交换机,队列及用户的情况。

linux 上可以通过命令的形式进行查看
在这里插入图片描述

消息追踪

消息追踪视频
在这里插入图片描述

Firehose

在这里插入图片描述

rabbitmq_tracing

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值