Loading

消息的可靠性

消息确认机制

确认并且保证消息被送达,提供了两种方式:发布确认和事务。(两者不可同时使用)在channel为事务时,不可引入确认模式;同样channel为确认模式下,不可使用事务。

发布确认

有两种方式:消息发送成功确认和消息发送失败回调。

  • 消息发送成功确认

在spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml

connectionFactory 中启用消息确认:

<!-- publisher-confirms="true" 表示:启用了消息确认 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
        port="${rabbitmq.port}"
        username="${rabbitmq.username}"
        password="${rabbitmq.password}"
        virtual-host="${rabbitmq.virtual-host}"
        publisher-confirms="true" />

配置消息确认回调方法如下:

<!-- 消息回调处理类 -->
<bean id="confirmCallback" class="com.itheima.rabbitmq.MsgSendConfirmCallBack"/>
<!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
<!-- confirm-callback="confirmCallback" 表示:消息失败回调 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory" 
        confirm-callback="confirmCallback"/>

消息确认回调方法com.itheima.rabbitmq.MsgSendConfirmCallBack如下:

public class MsgSendConfirmCallBack implements RabbitTemplate.ConfirmCallback {
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            System.out.println("消息确认成功....");
        } else {
            //处理丢失的消息
            System.out.println("消息确认失败," + cause);
        }
    }
}

功能测试如下:

发送消息

com.itheima.rabbitmq.ProducerTest#queueTest

    @Test
    public void queueTest(){
        //路由键与队列同名
        rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息。");
    }

管理界面确认消息发送成功

消息确认回调

  • 消息发送失败回调

在spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml

connectionFactory 中启用回调:

<!-- publisher-returns="true" 表示:启用了失败回调 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
        port="${rabbitmq.port}"
        username="${rabbitmq.username}"
        password="${rabbitmq.password}"
        virtual-host="${rabbitmq.virtual-host}"
        publisher-returns="true" />

配置消息失败回调方法如下:

注意:同时需配置mandatory="true",否则消息则丢失

<!-- 消息失败回调类 -->
<bean id="sendReturnCallback" class="com.itheima.rabbitmq.MsgSendReturnCallback"/>
<!-- return-callback="sendReturnCallback" 表示:消息失败回调 ,同时需配置mandatory="true",否则消息则丢失-->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
        confirm-callback="confirmCallback" return-callback="sendReturnCallback" 
        mandatory="true"/>

消息失败回调方法com.itheima.rabbitmq.MsgSendReturnCallback如下:

public class MsgSendReturnCallback implements RabbitTemplate.ReturnCallback {
    public void returnedMessage(Message message, int i, String s, String s1, String s2) {
        String msgJson  = new String(message.getBody());
        System.out.println("Returned Message:"+msgJson);
    }
}

功能测试如下:

模拟消息发送失败

com.itheima.rabbitmq.ProducerTest#testFailQueueTest

@Test
public void testFailQueueTest() throws InterruptedException {
    //exchange 正确,queue 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE
    amqpTemplate.convertAndSend("test_fail_exchange", "", "测试消息发送失败进行确认应答。");
}

失败回调结果如下:

事务支持

场景:业务处理伴随消息的发送,业务处理失败(事务回滚)后要求消息不发送。rabbitmq 使用调用者的外部事务,通常是首选,因为它是非侵入性的(低耦合)。

外部事务的配置:spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml

<!-- channel-transacted="true" 表示:支持事务操作 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
       confirm-callback="confirmCallback" return-callback="sendReturnCallback"
       channel-transacted="true" /><!--平台事务管理器-->
<bean id="transactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
        <property name="connectionFactory" ref="connectionFactory"/>
</bean>
  • 模拟业务处理失败的场景:

测试类或者测试方法上加入@Transactional注解

@Transactional
public class ProducerTest
@Test
public void queueTest2(){
    //路由键与队列同名
    rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息--01。");
    System.out.println("----------------dosoming:可以是数据库的操作,也可以是其他业务类型的操作---------------");
    //模拟业务处理失败
    System.out.println(1/0);
    rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息--02。");
}

测试结果:

Consumer Ack

ack——acknowledge(vt. 承认;答谢;报偿;告知已收到),在RabbitMQ中指代的是消费者收到消息后确认的一种行为,关注点在于消费者能否实际接收到MQ发送的消息。

其提供了三种确认方式:

  自动确认acknowledge="none":当消费者接收到消息的时候,就会自动给到RabbitMQ一个回执,告诉MQ我已经收到消息了,不在乎消费者接收到消息之后业务处理的成功与否。
  手动确认acknowledge="manual":当消费者收到消息后,不会立刻告诉RabbitMQ已经收到消息了,而是等待业务处理成功后,通过调用代码的方式手动向MQ确认消息已经收到。当业务处理失败,就可以做一些重试机制,甚至让MQ重新向消费者发送消息都是可以的。
  根据异常情况确认acknowledge="auto":该方式是通过抛出异常的类型,来做响应的处理(如重发、确认等布拉不拉布拉)。这种方式比较麻烦。
当消息一旦被消费者接收到,会立刻自动向MQ确认接收,并将响应的message从RabbitMQ消息缓存中移除,但是在实际的业务处理中,会出现消息收到了,但是业务处理出现异常的情况,在自动确认的模式下,该条业务处理失败的message就相当于被丢弃了。如果设置了手动确认,则需要在业务处理完成之后,手动调用channel.basicAck(),手动的签收,如果业务处理失败,则手动调用channel.basicNack()方法拒收,并让MQ重新发送该消息。

如果不做任何关于acknowledge的配置,默认就是自动确认签收的。

生产者端没有变化,能发消息就可以

    @Test
    public void testAck() {
 
//        //消费者接收到该消息,解析到true,就模拟调用channel.basicAck确认签收消息
//        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.test", "test msg send [true]");
 
        //消费者接收到该消息,解析到false,就模拟调用channel.basicNack,拒收消息,让MQ重发
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.test", "test msg send [false]");
    }

消费者端打开Ack模式

server:
  port: 2002
 
spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: xxxxxxxxx
    password: xxxxxxxxx
    virtual-host: /LeoLee
    listener:
      simple:
        acknowledge-mode: manual #消费者端确认模式:none自动确认 manual手动确认 auto通过抛出异常的类型,来做响应的处理
        concurrency: 1 #当前监听的数量
        max-concurrency: 5 #最大监听数量
        retry:
          enabled: true #是否支持重试
          max-attempts: 4 #最大重试次数,默认为3

消费者端创建一个listener并实现ChannelAwareMessageListener接口(其实也可以不实现该接口,只要@RabbitListener标记的方法,或者@RabbitListener标记的类+@RabbitHandler标记的方法的参数列表有[com.rabbitmq.client.Channel]和[org.springframework.amqp.core.Message]两个参数,都可以)

package com.leolee.rabbitmq.MsgListener;
 
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
 
import java.io.IOException;
 
/**
 * @ClassName AckListener
 * @Description: Consumer Ack
 * 1.设置手动确认签收:acknowledge-mode: manual, retry.enabled: true #是否支持重试
 * 2.实现ChannelAwareMessageListener接口,ChannelAwareMessageListener是MessageListener的子接口
 * 3.如果消息接收并处理完成,调用channel.basicAck()向MQ确认签收
 * 4.如果消息接收但是业务处理失败,调用channel.basicNack()拒收,要求MQ重新发送
 * @Author LeoLee
 * @Date 2020/11/7
 * @Version V1.0
 **/
@Component
public class AckListener implements ChannelAwareMessageListener {
 
 
    @RabbitListener(queues = "boot_queue")
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
 
        Thread.sleep(1000);
        boolean tag = new String(message.getBody()).contains("true");
        System.out.println("接收到msg:" + new String(message.getBody()));
        //获取mes deliveryTag
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
 
        try {
            if (tag) {
                System.out.println("业务处理成功");
                //手动签收
                /*
                 * deliveryTag:the tag from the received {@link com.rabbitmq.client.AMQP.Basic.GetOk} or {@link com.rabbitmq.client.AMQP.Basic.Deliver}
                 * multiple: ture确认本条消息以及之前没有确认的消息,false仅确认本条消息
                 */
                channel.basicAck(deliveryTag, false);
            } else {
                //模拟业务处理失败抛出异常
                System.out.println("业务处理失败");
                throw new IOException("业务处理失败");
            }
        } catch (IOException e) {
            e.printStackTrace();
            /*
             * deliveryTag:the tag from the received {@link com.rabbitmq.client.AMQP.Basic.GetOk} or {@link com.rabbitmq.client.AMQP.Basic.Deliver}
             * multiple: ture确认本条消息以及之前没有确认的消息,false仅确认本条消息
             * requeue: true该条消息重新返回MQ queue,MQ broker将会重新发送该条消息
             */
            channel.basicNack(deliveryTag, false, true);
            //也可以使用channel.basicReject(deliveryTag, requeue),它只能拒收单条消息
            //channel.basicReject(deliveryTag, true);
        }
 
    }
}
View Code

用生产者测试类发送不同的消息给MQ成功接收并手动确认后,MQ队列就删除了该消息的缓存,被拒绝的消息一直发送.

消费端限流

场景

在处理秒杀场景时经常会用到rabbitmq削峰限流作用,假设我们的系统能每秒处理1000个请求,如果有上万个请求同时打进来,会造成服务器的瘫痪。

这时就需要在系统之前加一次处理,将请求发送的MQ中,再让A系统以每秒1000的速率去请求mq服务器。

具体实现

  • 配置prefetch数量
  • 确认方式设置为手动确认

yml

spring.application.name=springboot_rabbitmq
spring.rabbitmq.host=192.168.0.102
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
spring.rabbitmq.virtual-host=/
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.prefetch=2

消费者代码

@Component
@RabbitListener(queuesToDeclare = @Queue(name = "springboot-limit"))
public class CurrentlimitCustomer {
 
    @RabbitHandler
    public void receive(String msg, Channel channel, Message message) throws IOException {
 
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            Thread.sleep(1000*10);
            System.out.println("=====限流====>");
            System.out.println(msg);
            System.out.println(channel);
            System.out.println(message);
            //手动签收[参数1:消息投递序号,参数2:批量签收]
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
 
        }
    }
}

注意:消费者如果不设置 channel.basicAck(deliveryTag, true); 进行消息确认,则永远不用从消息队列中再取消息

生产者代码

@Test
    public void test09() throws Exception {
        for (int i = 0; i < 10 ; i++) {
            rabbitTemplate.convertAndSend("springboot-limit",  "限流测试");
        }
        Thread.sleep(1000 * 1000);
    }

效果图

 

posted @ 2021-08-05 14:35  1640808365  阅读(93)  评论(0编辑  收藏  举报