RabbitMQ--实战

一、原生代码小Demo

  pom:

        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.9.0</version>
        </dependency>

  producer:

        // 1. 创建一个 ConnectionFactory 工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.124.8");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("lcl");
        factory.setPassword("123456");
        // 2. 创建一个 Connection
        Connection conn = factory.newConnection();
        // 3. 获取一个信道 Channel
        Channel channel = conn.createChannel();
        // 声明一个交换机
        channel.exchangeDeclare("cc", BuiltinExchangeType.DIRECT,true);
        // 4. 通过 Channel 发送消息
        String msg = "cuihuaaaa";
        for (int i = 0; i < 100; i++) {
            channel.basicPublish("cc", "hello", null, msg.getBytes());
            System.out.println(i);
        }
        // 5. 关闭资源
        channel.close();
        conn.close();

  consumer:

        // 1. 创建 ConnetionFactory 连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //factory.setUsername("guest");
        //factory.setPassword("guest");
        factory.setHost("192.168.124.8");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("lcl");
        factory.setPassword("123456");
        // 2. 获取 Connection 连接对象
        Connection connection = factory.newConnection();
        // 3. 创建 Channel 信道
        Channel channel = connection.createChannel();
        // 声明交换机
        String exchangeName = "aa";
        channel.exchangeDeclare(exchangeName, "direct", true);
        // 4. 声明队列
        String queueName = channel.queueDeclare().getQueue();
        String routingKey = "msg02";
        channel.queueBind(queueName, exchangeName, routingKey);
        // 5. 消费消息
        while (true){
            boolean autoAck = false;
            String consmerTag = "";
            channel.basicConsume(queueName, autoAck, consmerTag, new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    // super.handleDelivery(consumerTag, envelope, properties, body);
                    String routingKey = envelope.getRoutingKey();
                    String contentType = properties.getContentType();
                    System.out.println("消费的路由键:" + routingKey + " 消费的内容类型:" + contentType);
                    long deliveryTag = envelope.getDeliveryTag();
                    // 确认消息
                    channel.basicAck(deliveryTag, false);
                    System.out.println("消费的消息体:");
                    String bodyMsg = new String(body, "UTF-8");
                    System.out.println(bodyMsg);

                }
            });
        }
    }

二、不同类型交换机的使用及Springboot集成RabbitMQ

  pom:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

  application.yml:

spring:
  #配置rabbitMq 服务器
  rabbitmq:
    host: 192.168.124.8
    port: 5672
    username: lcl
    password: 123456
    #虚拟host 可以不设置,使用server默认host
    virtual-host: /

  然后就是具体的实现了。

(一)直连型交换机Direct Exchange

  直连交换机就是一对一,一个交换机对应一个队列,然后消费者消费指定队列的消息。

  1、创建配置文件

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectRabbitConfig {
    //队列 起名:TestDirectQueue
    @Bean
    public Queue TestDirectQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //   return new Queue("TestDirectQueue",true,true,false);
        //一般设置一下队列的持久化就好,其余两个就是默认false
        return new Queue("TestDirectQueue",true);
    }
    //Direct交换机 起名:TestDirectExchange
    @Bean
    DirectExchange TestDirectExchange() {
        //  return new DirectExchange("TestDirectExchange",true,true);
        return new DirectExchange("TestDirectExchange",true,false);
    }
    //绑定  将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectRouting");
    }
    @Bean
    DirectExchange lonelyDirectExchange() {
        return new DirectExchange("lonelyDirectExchange");
    }
}

    (1)对于创建队列,源码如下

        durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效

        exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable

        autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。

      一般设置一下队列的持久化就好,其余两个就是默认false

      

    (2) 对于创建交换机,源码如下:

        durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效

        autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。

      

     (3)交换机和队列绑定

  2、Producer

@RestController
public class SendMessageApi {
    @Autowired
    RabbitTemplate rabbitTemplate;  //使用RabbitTemplate,这提供了接收/发送等等方法
    @GetMapping("/direct")
    public String sendDirectMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "test message, hello!";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String,Object> map=new HashMap<>();
        map.put("messageId",messageId);
        map.put("messageData",messageData);
        map.put("createTime",createTime);
        //将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
        rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectRouting", map);
        return "ok";
    }
}

  3、consumer

  这里也需要配置相关的ip和用户等信息,我这里是将生产者和消费者放到了同一个项目中,上面已经配置过,这里就不需要在配置了。

@Component
@RabbitListener(queues = "TestDirectQueue")//监听的队列名称 TestDirectQueue
public class DirectConsumer {
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("DirectReceiver消费者收到消息  : " + testMessage.toString());
    }
}

(二)主题交换机Topic Exchange

  主题交换机与直连交换机的区别就是可以一对多,也就是一个交换机对应多个队列,然后消费者消费指定队列的消息。

  这里模拟分别有三个队列,分别是topic.man、topic.woman、topic.man.lcl,统一都绑定到一个交换机上:

    一个是用全匹配绑定(topic.man),只有发送的routingkey为topic.man时,才会被路由到该队列

    一个是#模糊匹配(topic.#),也就只要routingkey为topic.(后面只有一个单词),则会被路由到队列

    一个是*模糊匹配(topic.*),也就是只要routingkey为topic.开头(后面可以有任意个单词),则会被路由到该队列

  1、创建配置文件

@Configuration
public class TopicRabbitConfig {
    //绑定键
    public final static String man = "topic.man";
    public final static String woman = "topic.woman";
    public final static String lcl = "topic.man.lcl";
    @Bean
    public Queue firstQueue() {
        return new Queue(TopicRabbitConfig.man);
    }
    @Bean
    public Queue secondQueue() {
        return new Queue(TopicRabbitConfig.woman);
    }
    @Bean
    public Queue lclQueue() {
        return new Queue(TopicRabbitConfig.lcl);
    }
    @Bean
    TopicExchange exchange() {
        return new TopicExchange("topicExchange");
    }
    //将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
    //这样只要是消息携带的路由键是topic.man,才会分发到该队列
    @Bean
    Binding bindingExchangeMessage() {
        return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);
    }
    //将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
    // 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列(topic后只能有任意个词)
    @Bean
    Binding bindingExchangeMessage2() {
        return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
    }
    //将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.*
    // 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列(topic后只能有一个词)
    @Bean
    Binding bindingExchangeMessage3() {
        return BindingBuilder.bind(lclQueue()).to(exchange()).with("topic.*");
    }
}

  2、Producer

  发送三个消息,routingkey分别为topic.man、topic.woman、topic.man.lcl

    @GetMapping("/topic1")
    public String sendTopicMessage1() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: M A N ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> manMap = new HashMap<>();
        manMap.put("messageId", messageId);
        manMap.put("messageData", messageData);
        manMap.put("createTime", createTime);
        rabbitTemplate.convertAndSend("topicExchange", "topic.man", manMap);
        return "ok";
    }

    @GetMapping("/topic2")
    public String sendTopicMessage2() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: woman is all ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> womanMap = new HashMap<>();
        womanMap.put("messageId", messageId);
        womanMap.put("messageData", messageData);
        womanMap.put("createTime", createTime);
        rabbitTemplate.convertAndSend("topicExchange", "topic.woman", womanMap);
        return "ok";
    }

    @GetMapping("/topic3")
    public String sendTopicMessage3() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: M A N ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> manMap = new HashMap<>();
        manMap.put("messageId", messageId);
        manMap.put("messageData", messageData);
        manMap.put("createTime", createTime);
        rabbitTemplate.convertAndSend("topicExchange", "topic.man.lcl", manMap);
        return "ok";
    }

  3、Consumer

  创建三个消费者,消费的队列分别为topic.man、topic.woman、topic.man.lcl

@Component
@RabbitListener(queues = "topic.man")
public class TopicManReceiver {
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("TopicManReceiver消费者收到消息  : " + testMessage.toString());
    }
}
@Component
@RabbitListener(queues = "topic.woman")
public class TopicWomanReceiver {
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("TopicWomanReceiver消费者收到消息  : " + testMessage.toString());
    }
}
@Component
@RabbitListener(queues = "topic.man.lcl")
public class TopicLclReceiver {
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("TopicLclReceiver消费者收到消息  : " + testMessage.toString());
    }
}

  4、调用结果

    调用方法一,发送的是topic.man,由于三个队列的routingkey分别为topic.man、topic.#、topic.*,因此三个队列都会有信息路由到。

    调用方法二,发送的是topic.woman,topic.man肯定路由不到,topic.#、topic.*符合路由规则,因此这两个队列都会有信息路由到。

    调用方法三,发送的是topic.man.lcl,只有topic.*被路由到,因此只有这一个队列会有消息。

      

(三)广播交换机Fanout

  广播交换机的特点就是不需要设置routingkey,直接就可以将交换机的消息路由到绑定的队列上,这里演示同一个交换机绑定多个队列。

  1、创建配置文件

@Configuration
public class FanoutRabbitConfig {
    @Bean
    public Queue queueA() {
        return new Queue("fanout.A");
    }
    @Bean
    public Queue queueB() {
        return new Queue("fanout.B");
    }
    @Bean
    public Queue queueC() {
        return new Queue("fanout.C");
    }
    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanoutExchange");
    }
    @Bean
    Binding bindingExchangeA() {
        return BindingBuilder.bind(queueA()).to(fanoutExchange());
    }
    @Bean
    Binding bindingExchangeB() {
        return BindingBuilder.bind(queueB()).to(fanoutExchange());
    }
    @Bean
    Binding bindingExchangeC() {
        return BindingBuilder.bind(queueC()).to(fanoutExchange());
    }
}

  2、Producer

    @GetMapping("/fanout")
    public String sendFanoutMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: testFanoutMessage ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        rabbitTemplate.convertAndSend("fanoutExchange", null, map);
        return "ok";
    }

  3、Consumer

  分别创建三个消费者,消费绑定的三个队列

@Component
@RabbitListener(queues = "fanout.A")
public class FanoutReceiverA {
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("FanoutReceiverA消费者收到消息  : " +testMessage.toString());
    }
}
@Component
@RabbitListener(queues = "fanout.B")
public class FanoutReceiverB {
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("FanoutReceiverB消费者收到消息  : " +testMessage.toString());
    }
}
@Component
@RabbitListener(queues = "fanout.C")
public class FanoutReceiverC {
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("FanoutReceiverC消费者收到消息  : " +testMessage.toString());
    }
}

  4、演示结果

    发送一条消息后,三个消费端均可以接收

      

 三、消息的可靠性

  消息的可靠性分为投递的可靠性和接收可靠性,也就是生产者如何保证消息发送成功而不丢失,消费者如何可以正确的消费消息。

(一)消息投递可靠性分析

   1、方案一:使用消息确认监听

      

     发送消息时,将业务数据和消息数据一起入库,并添加消息监听,如果消息被确认接收成功,则更新数据库中消息的状态。同时使用定时任务轮询那些没有被确认的消息,可以重新发送。

    但是这种场景有个问题,就是在大并发的场景下,会对数据库造成很大的压力,从而造成系统瓶颈,这里可以使用数据库的分库分表从而降低数据库的压力。

  2、方案二:消息延迟投递、做二次确认,回调检查

      

     先将消息落库,然后再发送消息,这里发送了两条消息,一条是真正的业务消息,一条是原消息做延迟检查的消息,一般情况下会延迟2-5分钟发送。

    消费者接收到消息后,进行消费,然后消费完成后给予确认

    CallBack服务通过监听器,监听到消费者的确认之后,对消息做最终的存储

    当接收到延迟投递检查时,callback服务监听到检查的消息,开始进行处理,如果已经接收到消费者消费完成的确认信息,则完成本次检查的消费;如果没有收到消费者消费确认信息,这时callback需要做补偿,其会主动发起RPC通信,让生产者再发送一次。

    这么做的目的,其实就是少一次DB操作,从而减少数据库的压力。

(二)生产者Confirm消息确认与Return机制

  消息确认是指在生产者投递消息后,如果broker收到消息,则会给生产者一个应答,生产者根据应答来确定消息是否已经被发送到broker

  消息的确认是通过异步的方式来确认的。

  消息确认和普通的发送消息有两点区别:需要开启确认模式和增加监听

  1、原生代码模式

        //4 指定我们的消息投递模式: 消息的确认模式
        channel.confirmSelect();
        //5 发送一条消息
        channel.basicPublish(exchangeName, routingKey, null, msg.getBytes());
        System.err.println("------- 发送完成 ----------");
        //6 添加一个确认监听
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("------- 没有 ACK ----------");
            }
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("------- 收到 ACK -----------");
            }
        });

  2、SpringBoot代码

   (1)application.yml配置文件

spring:
  #配置rabbitMq 服务器
  rabbitmq:
    host: 192.168.124.8
    port: 5672
    username: lcl
    password: 123456
    #虚拟host 可以不设置,使用server默认host
    virtual-host: /

    #消息确认配置项
    #确认消息已发送到交换机(Exchange) 老版本使用配置项 publisher-confirms: true
    publisher-confirm-type: correlated
    #确认消息已发送到队列(Queue)
    publisher-returns: true

  (2)配置代码

@Configuration
public class ConfirmRabbitConfig {
    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        //设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);

        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("ConfirmCallback:     "+"相关数据:"+correlationData);
                System.out.println("ConfirmCallback:     "+"确认情况:"+ack);
                System.out.println("ConfirmCallback:     "+"原因:"+cause);
            }
        });

        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("ReturnCallback:     "+"消息:"+message);
                System.out.println("ReturnCallback:     "+"回应码:"+replyCode);
                System.out.println("ReturnCallback:     "+"回应信息:"+replyText);
                System.out.println("ReturnCallback:     "+"交换机:"+exchange);
                System.out.println("ReturnCallback:     "+"路由键:"+routingKey);
            }
        });

        return rabbitTemplate;
    }
}

    这里强调一下,一定要加上rabbitTemplate.setMandatory(true);或者在配置文件中设置spring.rabbitmq.template.mandatory=true,return消息机制才会生效。

    可以看到上面写了两个回调函数,一个叫 ConfirmCallback ,一个叫 RetrunCallback; 那么以上这两种回调函数都是在什么情况会触发呢?

    推送消息存在四种情况:

      (1)消息推送到server,但是在server里找不到交换机

      (2)消息推送到server,找到交换机了,但是没找到队列

      (3)消息推送到sever,交换机和队列啥都没找到

      (4)消息推送成功

    那么我先写几个接口来分别测试和认证下以上4种情况,消息确认触发回调函数的情况

    (1)消息推送到server,但是在server里找不到交换机

      写个测试接口,把消息推送到名为‘non-existent-exchange’的交换机上(这个交换机是没有创建没有配置的):

    @GetMapping("/ack")
    public String TestMessageAck() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: non-existent-exchange test message ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        rabbitTemplate.convertAndSend("non-existent-exchange", "TestDirectRouting", map);
        return "ok";
    }

    输出结果:这种情况触发的是 ConfirmCallback 回调函数。并且由于没有对应的交换机,因此返回false

      

     (2)消息推送到server,找到交换机了,但是没找到队列

      这种情况就是需要新增一个交换机,但是不给这个交换机绑定队列,由于之前已经在DirectRabitConfig里面新增一个直连交换机,名叫‘lonelyDirectExchange’,但没给它做任何绑定配置操作

    @Bean
    DirectExchange lonelyDirectExchange() {
        return new DirectExchange("lonelyDirectExchange");
    }

      测试方法:

    @GetMapping("/ack2")
    public String TestMessageAck2() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: lonelyDirectExchange test message ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        rabbitTemplate.convertAndSend("lonelyDirectExchange", "TestDirectRouting", map);
        return "ok";
    }

      调用结果:

      

     可以看到这种情况,两个函数都被调用了; 这种情况下,消息是推送成功到服务器了的,所以ConfirmCallback对消息确认情况是true; 而在RetrunCallback回调函数的打印参数里面可以看到,消息是推送到了交换机成功了,但是在路由分发给队列的时候,找不到队列,所以报了错误 NO_ROUTE 。 

    (3)消息推送到sever,交换机和队列啥都没找到 这种情况其实和(1)一样。

      结论: (3)这种情况触发的是 ConfirmCallback 回调函数。

    (4)消息推送成功

    那么测试下,按照正常调用之前消息推送的接口就行,就调用下 /direct接口

      

     结论: 这种情况触发的是 ConfirmCallback 回调函数。

(三)消费端ack与重回队列

  和生产者的消息确认机制不同,因为消息接收本来就是在监听消息,符合条件的消息就会消费下来。

  消息接收的确认机制主要存在三种模式:

    1、自动确认, 这也是默认的消息确认情况。 AcknowledgeMode.NONE RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。 所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。 一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。

    2、根据情况确认, 这个不做介绍

    3、手动确认 , 这个比较关键,也是我们配置接收消息确认机制时,多数选择的模式。

      消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功。

      (1)basic.ack用于肯定确认

      (2)basic.nack用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)

      (3)basic.reject用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息

  消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理。 而basic.nack,basic.reject表示没有被正确处理。

  着重讲下reject,因为有时候一些场景是需要重新入列的。

    channel.basicReject(deliveryTag, true); 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了。

    使用拒绝后重新入列这个确认模式要谨慎,因为一般都是出现异常的时候,catch异常再拒绝入列,选择是否重入列。 但是如果使用不当会导致一些每次都被你重入列的消息一直消费-入列-消费-入列这样循环,会导致消息积压。

  顺便也简单讲讲 nack,这个也是相当于设置不消费某条消息。

    channel.basicNack(deliveryTag, false, true); 第一个参数依然是当前消息到的数据的唯一id; 第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。 第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。 同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。

  1、原生代码

            channel.basicConsume(queueName, autoAck, consumerTag, new DefaultConsumer(channel){
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    // 获取传送标签
                    // long deliveryTag = envelope.getDeliveryTag();
                    // 确认消息
                    // channel.basicAck(deliveryTag, false);
                    System.out.println("消费的 Body:" + new String(body, "UTF-8"));
                }
            });

  2、Springboot代码

  (1)添加一个手动确认类

    如果成功,返回肯定确认,如果catch到异常,返回否定确认。

@Component
public class MyAckReceiver implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            //因为传递消息的时候用的map传递,所以将Map从Message内取出需要做些处理
            String msg = message.toString();
            String[] msgArray = msg.split("'");//可以点进Message里面看源码,单引号直接的数据就是我们的map消息数据
            Map<String, String> msgMap = mapStringToMap(msgArray[1].trim(),3);
            String messageId=msgMap.get("messageId");
            String messageData=msgMap.get("messageData");
            String createTime=msgMap.get("createTime");
            System.out.println("  MyAckReceiver  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
            System.out.println("消费的主题消息来自:"+message.getMessageProperties().getConsumerQueue());
            channel.basicAck(deliveryTag, true); //第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
//            channel.basicReject(deliveryTag, true);//第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝
            //根据不同的队列走不同的消费逻辑
            /*if ("TestDirectQueue".equals(message.getMessageProperties().getConsumerQueue())){
                System.out.println("消费的消息来自的队列名为:"+message.getMessageProperties().getConsumerQueue());
                System.out.println("消息成功消费到  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
                System.out.println("执行TestDirectQueue中的消息的业务处理流程......");

            }
            if ("fanout.A".equals(message.getMessageProperties().getConsumerQueue())){
                System.out.println("消费的消息来自的队列名为:"+message.getMessageProperties().getConsumerQueue());
                System.out.println("消息成功消费到  messageId:"+messageId+"  messageData:"+messageData+"  createTime:"+createTime);
                System.out.println("执行fanout.A中的消息的业务处理流程......");

            }*/
        } catch (Exception e) {
            channel.basicReject(deliveryTag, false);
            e.printStackTrace();
        }
    }
    //{key=value,key=value,key=value} 格式转换成map
    private Map<String, String> mapStringToMap(String str,int entryNum ) {
        str = str.substring(1, str.length() - 1);
        String[] strs = str.split(",",entryNum);
        Map<String, String> map = new HashMap<String, String>();
        for (String string : strs) {
            String key = string.split("=")[0].trim();
            String value = string.split("=")[1];
            map.put(key, value);
        }
        return map;
    }

  (2)添加配置代码

@Configuration
public class MessageAckListenerConfig {
    @Autowired
    private CachingConnectionFactory connectionFactory;
    @Autowired
    private MyAckReceiver myAckReceiver;//消息接收处理类
    @Bean
    public SimpleMessageListenerContainer simpleMessageListenerContainer() {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        container.setConcurrentConsumers(1);
        container.setMaxConcurrentConsumers(1);
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // RabbitMQ默认是自动确认,这里改为手动确认消息
        //设置一个队列
        container.setQueueNames("TestDirectQueue");
        //如果同时设置多个如下: 前提是队列都是必须已经创建存在的
        //  container.setQueueNames("TestDirectQueue","TestDirectQueue2","TestDirectQueue3");
        //另一种设置队列的方法,如果使用这种情况,那么要设置多个,就使用addQueues
        //container.setQueues(new Queue("TestDirectQueue",true));
        //container.addQueues(new Queue("TestDirectQueue2",true));
        //container.addQueues(new Queue("TestDirectQueue3",true));
        container.setMessageListener(myAckReceiver);
        return container;
    }
}

    调用接口验证

      

   (3)说明

    上面的代码演示的是只有一个队列,但是有的业务场景需要设置多个队列手动确认,那么就可以参照注释掉的代码,在确认时,根据不同的队列名进行手动确认,同时在配置时,设置多个队列

四、消费端消息限流

  消费端限流实际上就是消息在消费端的平稳投递消费,而不会导致一时间大量的消息打入到消费端,给消费端造成压力。

  RabbitMQ 提供了一种 qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于 consumer 或者 channel 设置 Qos 的值)未被确认前,不进行消费新的消息。

  在原生代码中,对channel进行设置

        /**
         * 限流设置:  prefetchSize:每条消息大小的设置,0是无限制
         * prefetchCount:标识每次推送多少条消息 一般是一条
         * global:false标识channel级别的  true:标识消费者级别的
         */
        channel.basicQos(0,1,false);

  在Springboot中就更简单了,只需要对配置文件做调整即可

spring:
  #配置rabbitMq 服务器
  rabbitmq:
    listener:
      simple:
        #消费者最小数量
        concurrency: 1
        #消费之最大数量
        max-concurrency: 10
        #在单个请求中处理的消息个数,他应该大于等于事务数量(unack的最大数量)
        prefetch: 1
        # 是否手动确认
        #acknowledge-mode: manual

五、特殊队列(TTL队列与死信队列)

(一)TTL队列

  TTL 是 Time To Live 的所写,也就是生存时间,RabbitMQ 支持消息的过期时间,在消息发送时可以进行指定。

  RabbitMQ 支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会自动地清除。

  生产者在发送时设置消息的有效期

Builder bd = new AMQP.BasicProperties().builder();
bd.deliveryMode(2);//持久化
bd.expiration("100000");//设置消息有效期100秒钟
BasicProperties pros = bd.build();
String message = "测试ttl消息";
channel.basicPublish(EXCHANGE_NAME, "error", true,false, pros, message.getBytes());

  消费者设置队列中消息的有效期

//设置队列上所有的消息的有效期,单位为毫秒
Map<String, Object> argss = new HashMap<String , Object>();
arguments.put("x-message-ttl " , 5000);//5秒钟
channel.queueDeclare(queueName , durable , exclusive , autoDelete , arguments) ;

(二)死信队列

  死信队列 DLX, Dead-Letter-Exchange,利用 DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新 pulish 到另一个 Exchange,这个 Exchange 就是 DLX。

  当某个队列中有死信时,RabbitMQ 就会自动的将这个消息重新发布到设置的 Exchange 上去,进而被路由到另一个队列。

  死信队列也叫延时队列。基于TTL模式的延时队列会涉及到2个交换机、2个路由键、2个队列,如下图所示:

    (1)生产者将消息(msg)和路由键(routekey)发送指定的死信交换机(DelayExchange)上

    (2)死信交换机(delayexchange)根据路由键(routekey1)找到绑定自己的死信队列(DelayQueue)并把消息给它

    (3)消息(msg)到期死亡变成死信转发给死信接收交换机(ReceiveExchange)

    (4)死信接收交换机(ReceiveExchange)根据路由键(routekey2)找到绑定自己的死信接收队列(ReceiveQueue)并把消息给它

    (5)死信接收队列(ReceiveQueue)再把消息发送给监听它的消费者(customer)

      

   消息变成死信队列的几种情况:

    1、消息被拒绝(basic.reject / basic.nack)并且 requeue = false 

    2、消息 TTL 过期

    3、队列达到最大长度

  DLX 也是一个正常的 Exchange,和一般的 Exchange 没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。

   代码样例:

    1、配置队列绑定

      分别创建死信队列交换机、接收对接交换机、死信队列、接收队列以及绑定关系

@Configuration
public class DelayRabbitConfig {

    /**
     * 死信交换机
     * @return
     */
    @Bean
    public DirectExchange delayExchange(){
        return new DirectExchange("delay_exchange");
    }

    /**
     * 死信队列
     * @return
     */
    @Bean
    public Queue delayQueue(){
        Map<String,Object> map = new HashMap<>(16);
        map.put("x-dead-letter-exchange","receive_exchange");
        map.put("x-dead-letter-routing-key", "receive_key");
        return new Queue("delay_queue",true,false,false,map);
    }

    /**
     * 给死信队列绑定交换机
     * @return
     */
    @Bean
    public Binding delayBinding(Queue delayQueue, DirectExchange delayExchange){
        return BindingBuilder.bind(delayQueue).to(delayExchange).with("delay_key");
    }

    /**
     * 死信接收交换机
     * @return
     */
    @Bean
    public DirectExchange receiveExchange(){
        return new DirectExchange("receive_exchange");
    }

    /**
     * 死信接收队列
     * @return
     */
    @Bean
    public Queue receiveQueue(){
        return new Queue("receive_queue");
    }

    /**
     * 死信交换机绑定消费队列
     * @return
     */
    @Bean
    public Binding receiveBinding(Queue receiveQueue,DirectExchange receiveExchange){
        return BindingBuilder.bind(receiveQueue).to(receiveExchange).with("receive_key");
    }

    上面的设置死信队列时的key是固定的,如果不清楚,可以参看以下控制台。

      

     2、Producer

    @GetMapping("/delay")
    public String sendDelayMessage() {
        //这里的消息可以是任意对象,无需额外配置,直接传即可
        String messageData = "sendDelayMessage~~~~~~~~~ ";
        log.info("===============延时队列生产消息====================");
        log.info("发送时间:{},发送内容:{}", LocalDateTime.now(), messageData);
        this.rabbitTemplate.convertAndSend(
                "delay_exchange",
                "delay_key",
                messageData,
                message -> {
                    //注意这里时间要是字符串形式
                    message.getMessageProperties().setExpiration("60000");
                    return message;
                }
        );
        log.info("{}ms后执行", 60000);
        return "OK~";
    }

    3、consumer

@Component
@Slf4j
public class DelayConsumer {

    @RabbitListener(queues = "receive_queue")
    public void cfgUserReceiveDealy(List<Integer> list, Message message, Channel channel) throws IOException {
        log.info("===============接收队列接收消息====================");
        log.info("接收时间:{},接受内容:{}", LocalDateTime.now(), list.toString());
        //通知 MQ 消息已被接收,可以ACK(从队列中删除)了
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        try {
            log.error("============dosomething.....!==============");
        } catch (Exception e) {
            log.error("============消费失败,尝试消息补发再次消费!==============");
            log.error(e.getMessage());
            /**
             * basicRecover方法是进行补发操作,
             * 其中的参数如果为true是把消息退回到queue但是有可能被其它的consumer(集群)接收到,
             * 设置为false是只补发给当前的consumer
             */
            channel.basicRecover(false);
        }
    }
}

    4、验证

      

 

posted @ 2021-08-17 15:01  李聪龙  阅读(389)  评论(0编辑  收藏  举报