返回顶部

RabbitMQ(二)高级特性

开始前要将第一篇中的准备工作都完成
RabbitMQ(一)安装与入门


前言

通过上图可知消息投递失败将会发生在三个地方,生产者到交换机,交换机到队列,队列到消费者。所以为了保证消息的可靠性,需要开启消息确认机制(confirmCallback、returnCallback)以及消费端手动确认模式(手动ack)或者消费者重试机制。

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

RabbitMQ 整个消息投递的路径为:

producer—>RabbitMQ broker—>exchange—>queue—>consumer

消息从 producer 到 exchange 则会返回一个 confirmCallback 。

消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。

将利用这两个 callback 控制消息的可靠性投递

注:因SpringBoot 整合RabbitMQ 当队列或交换机不存在时,自动创建,所以可靠性检测的一般是服务是否宕机。与消费者是否接收/确认消息无无关


一、消息可靠性投递(生产者端)

1.配置yml文件

spring:
  rabbitmq:
    host: 127.0.0.1 #ip地址
    port: 5672  #端口
    virtual-host: / #虚拟主机
    username: guest #账号
    password: guest #密码
    # 开启publisher-confirm(确认模式) 有以下可选值
    # simple:同步等待confirm结果,直到超时
    # correlated:异步回调,定义ConfirmCallback。mq返回结果时会回调这个ConfirmCallback
    # NONE:默认不开启
    publisher-confirm-type: correlated
    # 开启publish-return(回调模式)功能。可以定义ReturnCallback
    # true:调用ReturnCallback
    # false:直接丢弃消息
    publisher-returns: true

2.编写自定义Callback类

@Component
public class ConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
    @Resource
    RabbitTemplate rabbitTemplate;

    @PostConstruct  //@PostConstruct注解:实现Bean初始化之前的操作
    public void initMethod() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }
    
    /**
     * @param correlationData   相关配置消息
     * @param ack   表示exchange交换机是否收到了消息,true成功,false失败
     * @param cause 失败原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (null != correlationData) {
            correlationData.getReturned().getMessage().getMessageProperties().getReceivedDelay();
        }
        if (ack) {
            //接收成功
            System.out.println("confirm方法被执行了,消息已经送达Exchange,ack已发");
        } else {
            //接收失败
            System.out.println("confirm方法被执行了,消息送达失败Exchange,原因:" + cause);
        }
    }
    /**
     * 回退模式:当消息发送给Exchange后,Exchange路由到Queue失败后才执行returnedMessage
     * @param returnedMessage
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        System.out.println("returnedMessage方法被执行了,因为消息送到队列失败");
    }
}

3.运行测试

@SpringBootTest(classes = ProducerApplication.class)
@RunWith(SpringRunner.class)
public class ProducerTest {
    //1.注入RabbitTemplate
    @Resource
    private RabbitTemplate rabbitTemplate;
    //2.发送消息
    @Test
    public void testSend(){
        /*convertAndSend参数:
            交换机名
            routingKey可以理解为组名为queue,成员hello
            消息
        */
        rabbitTemplate.convertAndSend(EXCHANGE_NAME,"queue.hello","这是一个消息。。。。。。");
    }
}

3.1 测试结果


二、手动ACK确认机制(消费者者端)

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

有三种确认方式:

  • 自动确认acknowledge="none"

    消费者接收消息后立即ack,然后慢慢处理,当消费者重启或出现异常时会丢失消息。

  • 手动确认acknowledge="manual"

    消费者接收消息后,不会立刻告诉RabbitMQ已经收到消息了,而是等待业务处理成功后,通过调用代码的方式手动向MQ确认消息已经收到。当业务处理失败,就可以做一些重试机制,甚至让MQ重新向消费者发送消息都是可以的。

  • 根据异常情况确认acknowledge="auto"

    该方式是通过抛出异常的类型,来做响应的处理(如重发、确认等)。这种方式比较麻烦

1.配置yml文件

spring:
  rabbitmq:
    host: 127.0.0.1 #ip地址
    port: 5672  #端口号
    virtual-host: /
    username: guest #账号
    password: guest #密码
    listener:
	# 容器类型simple或direct, simple理解为一对一;direct理解为一对多个消费者
      simple:
        # ACK模式(none自动,auto抛异常,manual手动,默认为auto)
        acknowledge-mode: manual
        # 开启重试
        retry:
          # 是否开启重试机制
          enabled: true

2.编写消费者监听类

@Slf4j
@Component
public class RabbitMQListener {
    private static final int MAX_RETRIES = 3;//消息最大重试次数

    private static final long RETRY_INTERVAL = 3;//重试间隔(秒)
    
    /**
     * 手动进入死信队列
     * RabbitListener中的参数用于表示监听的是哪一个队列
     * ACK机制:
     * 如果消息消费成功,则调用channel的basicACK()签收
     * 如果消息消费失败,则调用channel的basicNack()拒绝签收
     */
    @RabbitListener(queues = "topic_queue")//监听的队列名
    public void ListenerQueue(Message msg, Channel channel) throws Exception {
        //消息的index
        long deliveryTag = msg.getMessageProperties().getDeliveryTag();
        // 重试次数
        int retryCount = 0;
        boolean success = false;
        // 消费失败并且重试次数<=重试上限次数
        while (!success && retryCount < MAX_RETRIES) {
            retryCount++;
            // 具体业务逻辑
            /**
             * 模拟业务
             * 模拟业务
             * 模拟业务
             * success = true or false
             */
            System.out.println("正在处理业务逻辑");
            // 如果失败则重试
            if (!success) {
                String errorTip = "第" + retryCount + "次消费失败" +
                        ((retryCount < 3) ? "," + RETRY_INTERVAL + "s后重试" : ",进入死信队列");
                log.error(errorTip);
                Thread.sleep(RETRY_INTERVAL * 1000);
            }
        }
        if (success) {
            // 消费成功,确认
            channel.basicAck(deliveryTag, false);//第二参数: 是否批量处理.true:将一次性ack所有小于等于deliveryTag的消息
            log.info("消息消费成功");
        } else {
            // requeue:false 手动拒绝,进入抛弃或进入死信队列
            channel.basicNack(deliveryTag, false, false);//第二参数:是否批量处理. 第三参数:拒绝后是否重新入队,如果设置为true ,则会添加在队列的末端
            log.info("消息消费失败");
        }
    }
}

3.运行消费者主程序测试结果


三、消费端限流QOS

1.配置yml文件

spring:
  rabbitmq:
    host: 127.0.0.1 #ip地址
    port: 5672  #端口号
    virtual-host: /
    username: guest #账号
    password: guest #密码
    listener:
      # 容器类型simple或direct simple理解为一对一;direct理解为一对多个消费者
      simple:
        # ACK模式(none自动,auto抛异常,manual手动,默认为auto)
        acknowledge-mode: manual
        #每次从队列获取消息数量为1
        prefetch: 1
        # 开启重试
        retry:
          # 是否开启重试机制
          enabled: true

2.编写测试类

@Slf4j
@Component
public class RabbitMQListener {
    
@RabbitListener(queues = "topic_queue")//监听的队列名
public void ListenerQueue2(Message msg, Channel channel) throws Exception {
    //消息的index
    long deliveryTag = msg.getMessageProperties().getDeliveryTag();

    System.out.println("正在处理业务");
    //为了能看出效果,休眠2秒
    Thread.sleep(2000);
    
    //确认
    channel.basicAck(deliveryTag,true);
    System.out.println(new String(msg.getBody()));
}

2.1 测试结果

基于上面代码,第二次输出时“正在处理业务”和getBody将会同时出现,或进入MQ管理界面http://localhost:15672/,点击导航栏Queues观察Messages列下的Total总消息数,会发现以1为单位递减


四、TTL

TTLTime To Live的缩写,含义为存活时间或者过期时间。即:

  • 当消息到达存活时间后,还没有被消费,会被自动清除。
  • RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
  • 消息过期后,只有消息在队列顶端,才会判断其是否过期(否则过期消息不会被移除)。
  • 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
  • 设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断 这一消息是否过期。
  • 如果两者都进行了设置,以时间短的为准。

1.队列过期时间

在配置队列时使用.ttl(10000)来设定TTL过期时间,单位为毫秒(ms)

    @Bean("queue")
    public Queue queue(){
        return QueueBuilder
                .durable(QUEUE_NAME)//durable持久化
                .ttl(100000)    //ttl过期时间100秒,单位ms
                .build();
    }

2.消息过期时间

在发送消息时实现new postProcessMessage()方法来设置消息过期时间

    @Test
    public void testSend3() throws Exception {
        /*convertAndSend参数:
            交换机名
            routingKey可以理解为组名为queue,成员hello
            消息
        */

    rabbitTemplate.convertAndSend(EXCHANGE_NAME, "queue.hello", "这是一个会过期的消息。。。",
            new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            message.getMessageProperties().setExpiration(String.valueOf(5000));//5秒
            return message;
        }
    });
    //ConfirmCallback是异步的,执行之后我们实际上已经关闭了rabbitmq资源 ,所以需要休眠方便测试
    Thread.sleep(2000);//2秒
    }

五、死信队列

死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以 被重新发送到另一个交换机,这个交换机就是DLX。

消息成为死信的三种情况(消息无法被消费):

  • 队列消息长度到达限制
  • 消费者拒接消费消息,basicNack/basicReject, 并且不把消息重新放入原目标队列,requeue=false
  • 原队列存在消息过期设置,消息到达超时时间未被消费

1.编写代码

队列绑定死信交换机: 给队列设置:

.deadLetterExchange(死信交换机名称)

.deadLetterRoutingKey(死信交换机routingKey)

  • 死信交换机和死信队列和普通的没有区别

  • 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列

@Configuration
public class RabbitMQConfig {
    public static final String EXCHANGE_NAME = "topic_exchange";//普通交换机
    public static final String QUEUE_NAME = "topic_queue";//普通队列
    public static final String QUEUE_DLX = "dlx_queue";//死信队列
    public static final String EXCHANGE_DLX = "dlx_exchange";//死信交换机
    public static final String DLX_ROUTINGKEY = "dlx.routing";//死信路由key
    //1.交换机
    @Bean("exchange")
    public Exchange exchange(){
        return ExchangeBuilder
                .topicExchange(EXCHANGE_NAME)
                .durable(true)  //durable持久化
                .build();
    }
    //死信交换机
    @Bean("dlxExchange")
    public Exchange dlxExchange(){
        return ExchangeBuilder
                .topicExchange(EXCHANGE_DLX)
                .durable(true)  //durable持久化
                .build();
    }
    //2.队列
    @Bean("queue")
    public Queue queue(){
        return QueueBuilder
                .durable(QUEUE_NAME)//durable持久化
                .ttl(10000)    //ttl过期时间,单位ms
                .deadLetterExchange(EXCHANGE_DLX)//绑定死信交换机
                .deadLetterRoutingKey(DLX_ROUTINGKEY)//绑定死信路由key,因为是队列向死信路由发消息
                .maxLength(10)//队列最大消息数量
                .build();
    }
    //死信队列
    @Bean("dlxQueue")
    public Queue dlxQueue(){
        return QueueBuilder
                .durable(QUEUE_DLX)//durable持久化
                .build();
    }
    //3.队列和交换机绑定
    @Bean
    public Binding bindQueueExchange(
            @Qualifier("queue") Queue queue,
            @Qualifier("exchange") Exchange exchange){

            return BindingBuilder
                    .bind(queue)//绑定队列
                    .to(exchange)//绑定交换机
                    .with("queue.*")//routingKey可以理解为组名为queue
                    .noargs();//不要参数
    }
    //死信队列和交换机绑定
    @Bean
    public Binding bindDlxQueueExchange(
            @Qualifier("dlxQueue") Queue dlxQueue,
            @Qualifier("dlxExchange") Exchange dlxExchange){
    
        return BindingBuilder
                .bind(dlxQueue)//绑定队列
                .to(dlxExchange)//绑定交换机
                .with("dlx.*")//routingKey可以理解为组名为queue
                .noargs();//不要参数
    }
}

2.测试

这里以超过队列最大消息数来测试

@Test
    public void testSend4() throws InterruptedException {
        /*convertAndSend参数:
            交换机名
            routingKey可以理解为组名为queue,成员hello
            消息
        */
        //20条消息超过了队列设置的最大数量10
        for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend(EXCHANGE_NAME, "queue.hello", "这是测试死信的消息");
        }
        //ConfirmCallback是异步的,执行之后我们实际上已经关闭了rabbitmq资源 ,所以需要休眠方便测试
        Thread.sleep(2000);
    }

2.1 测试结果


六、延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。因为RabbitMQ中为提供延迟队列功能,所以我们可以使用TTL + 死信队列的方式来实现延迟队列。

  • 需求

    下单后,X分钟未支付,取消订单,回滚库存。

  • 实现方式:

    TTL + 死信队列

1.编写代码

路由和队列绑定与上面的五、死信交换机相同,只修改监听方法

@RabbitListener(queues = "dlx_queue")//监听的队列名
    public void ListenerQueue2(Message msg, Channel channel) throws Exception {
        System.out.println("当前时间:" + LocalTime.now());
        //消息的index
        long deliveryTag = msg.getMessageProperties().getDeliveryTag();
        //接收消息内容
        System.out.println(new String(msg.getBody()));
        //处理业务
        System.out.println("正在处理业务...");
        System.out.println("判断状态...");
        System.out.println("是否取消...");
        //为了能看出效果,休眠2秒
        //Thread.sleep(2000);
        //确认
        channel.basicAck(deliveryTag, true);
    }

2.测试

@Test
    public void testDelaySend4() throws InterruptedException {
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, "queue.hello", "这是测试延时队列的消息:" + LocalTime.now());
        for (int i = 10; i > 0; i--) {
            System.out.println(i+"...");
            Thread.sleep(1000);
        }
    }

2.1 测试结果


七、消息轨迹追踪

使用消息踪迹追钟需要开启Tracing插件

1.开启插件

RabbitMQ默认安装了Tracing插件只要启用即可。

  • 进入MQ安装路径下的sbin目录
  • 打开命令行界面执行:rabbitmq-plugins enable rabbitmq_tracing

2.通过MQ管理页配置消息追踪

打开管理界面:http://localhost:15672/ ,如果没出现Tracing的可以重启一下MQ服务

  • Virtual host:需要追踪的虚拟路径
  • Format:日志文件格式(TEXT/JSON)
  • Max payload bytes:要记录的最大负载大小,以字节为单位。
  • Pattern:#匹配所有的消息,无论是发布还是消费的信息,publish.# 匹配所有发布的消息,deliver.# 匹配所有被消费的消息,#.test 如果test是队列,则匹配已经被消费了的test队列的消息。如果test是exchange,则匹配所有经过该exchange的消息。

配置完成后,点击Add Trace即可完成创建

3.测试

随便发送几条消息后,后回到Tracing界面点击.log文件,这时会要求输入账号密码,只要输入登录时的账号密码即可,如默认: guest/guest


八、应用问题

  • 消息补偿

    Producer:发送消息Q1和发送延迟消息Q3

    Consumer:接收消息Q1并发送确认消息Q2

    定时检查服务:

    第9步中:比对producer的db和消息确认的mdb,调用producer重发db中多的那些数据(即未发送成功或未被消费者成功确认的消息)

    回调检查服务:

    第6步中:监听到确认消息Q2,将消息写入数据库MDB

    第8步中:监听到延迟消息Q3,比对MDB中的消息,出现重复即代表该消息已被消费

  • 消息幂等性保障

    幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

    在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。

    乐观锁机制


posted @ 2023-04-11 16:08  r1se  阅读(102)  评论(0编辑  收藏  举报