RabbitMQ 操作测试

以下内容是团队要使用 RabbitMQ 的时候自己做的一些测试,并不是权威的,只是自己根据测试结果推测的结论。

一、使用 SpringCloud Stream 集成 RabbitMQ

1. 生产者 cloud-stream-rabbitmq-provider8801

1)pom

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
         <!--RabbitMQ-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
        <!--基础配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>    

2)配置文件

server:
  port: 8801

spring:
  application:
    name: cloud-stream-provider
  cloud:
      stream:
        binders: # 在此处配置要绑定的rabbitmq的服务信息;
          defaultRabbit: # 表示定义的名称,用于于binding整合
            type: rabbit # 消息组件类型
            environment: # 设置rabbitmq的相关的环境配置
              spring:
                rabbitmq:
                  host: localhost
                  port: 5672
                  username: user_lyd
                  password: 1234
                  virtual-host: /vhost_lyd
        bindings: # 服务的整合处理
          output: # 这个名字是一个通道的名称 (可以自己定义,这里是默认的)
            destination: testMaterialExchange # 表示要使用的 Exchange名称定义
            content-type: application/json # 设置消息类型,本次为 json,文本则设置 “text/plain”
            binder: defaultRabbit # 设置要绑定的消息服务的具体设置

eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://admin:admin@localhost:10002/eureka/eureka/
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: send-8801.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

注意上面写的交换机和队列,要创建好哦

3)发送消息的接口以及实现类

/**
 * @Description 发送消息接口
 */

public interface IMessageProvider {
    String send();
}
package com.janet.springcloud.service.impl;

import com.janet.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;

import javax.annotation.Resource;
import java.util.UUID;

/**
 * @Description 发送消息接口实现类
 */
@EnableBinding(Source.class) //定义消息的推送管道,指信道channel和exchange绑定到一起
public class MessageProviderImpl implements IMessageProvider {

    @Resource
    private MessageChannel output; //消息发送管道

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
        output.send(MessageBuilder.withPayload(serial).build());
        System.out.println("------发送消息:"+serial);
        return serial;
    }
}

4)生产者发送消息测试

import com.janet.springcloud.service.IMessageProvider;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
import javax.annotation.Resource;
 

@RestController
public class SendMessageController {
    @Resource
    private IMessageProvider messageProvider;
 
    @GetMapping(value = "/sendMessage")
    public String SendMessage(){
        return messageProvider.send();
    }
}

5)主启动类

@SpringBootApplication
public class StreamMQMain8801 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8801.class,args);
    }
}

2. 消费者1  cloud-stream-rabbitmq-consumer8802

1)POM

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--基础配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

2)配置文件

server:
  port: 8802

spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息;
        defaultRabbit: # 表示定义的名称,用于于binding整合
          type: rabbit # 消息组件类型
          environment: # 设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: user_lyd
                password: 1234
                virtual-host: /vhost_lyd
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称
          destination: testMaterialExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
          binder: defaultRabbit # 设置要绑定的消息服务的具体设置
 

eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://admin:admin@localhost:10002/eureka/eureka/
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: send-8801.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

3)主启动类

package com.janet.springcloud;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
/**
 * @Description 生产者1号主启动类
 */
@SpringBootApplication
public class StreamMQMain8802 {
    public static void main(String[] args) {
        SpringApplication.run(StreamMQMain8802.class,args);
    }
}

4)接收消息类

package com.janet.springcloud.controller;
 
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
 
/**
 * @Description TODO
 * @Author Janet
 * @Date 2020/6/27
 */
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController {
    @Value("${server.port}")
    private String serverPort;
 
    @StreamListener(Sink.INPUT)
    public void input(Message<String> message){
        // 用 withPayload 发送,用 getPayload 接收
        System.out.println("消费者 1 号,------------> 接收到的消息"+message.getPayload()+"\t port: "+serverPort);
    }
}

3. 消费者3 cloud-stream-rabbitmq-consumer8803

配置和消费者2一模一样

二、测试

1. 消费者队列名设置

1)两个消费者 group 属性都没有设置,消费者默认了两个不同的分组:

消费者发送了三条消息:

两个消费者都收到了这三条消息:

结论 1:如果消费者不设置分组信息,则每个消费者会默认不同的分组。不同的分组会重复消费生产者生产的消息。

2)两个消费者设置相同分组:

结论 2:此时两个消费者设置了同一个分组,连接了同一个队列,相当于负载均衡的情况,一个消息只能有一个消费者拿到。这里没有设置手动确认机制,默认了自动确认,可以看出消费者 1 和消费者 2 是有序的获取了消息。(轮询发送)

3)两个消费者在同一个组(绑定了同一个队列)的情况下,先关掉两个消费者的服务,生产者生产消息发送,再打开消费者1,过几秒后再打开消费者2,观察日志:

结论 3:在消费者绑定组的情况下,即使生产消息的时候不在线,后来在线了依然会收到消息(持久化)。毋庸置疑,如果消费者没有同时启动,一个先一个后的话,则只会有第一个消费者接收到全部消息。

4)两个消费者去掉组的属性,不绑定队列,然后像上一步一样,先发消息,再打开消费者服务:

此时可以看到消费者没有接收到上一次生产者发送的消息。

可以看到消费者各自有了默认的队列,如果此时关闭两个消费者服务,两个默认的队列就没了:

重启之后消费者又重新默认了一个新的队列,消费者自然也是接收不到上一次发送的信息的。


以下不用SpringCloud Stream 了,自己写一个队列。

2. 超时时间

2.1 队列设置过期时间

public class Send {
    private static final String EXCHANGE_NAME = "testMaterialExchange1";
    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"topic");

        for(int i = 0; i <10; i++){
            String msg = "hello mq"+i;
            System.out.println("生产者发送消息:"+msg);

            //4. 发送消息
            channel.basicPublish(EXCHANGE_NAME,"goods.new.add",null,msg.getBytes());

            System.out.println("----send"+msg);

        }

        channel.close();
        connection.close();
    }
}
public class Receive1 {
    private static final String EXCHANGE_NAME = "testMaterialExchange1";
    private static final String QUEUE_NAME = "testMaterialGroup3";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();
        
        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-message-ttl", 10000); //设置队列里面的消息过期时间10秒

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[1]: " + msg);

                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    channel.basicAck(envelope.getDeliveryTag(),false);
                    System.out.println("[1] done");
                }

            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

消费者创建队列时设置了 10 秒的超时时间,并且消费一个消息休眠 3 秒,生产者发送消息:

可以看到消费者只消费了 4 个消息之后队列中的消息都超时了,由于没有设置死信队列,消息消失了。

结论:当队列设置超时时间,并且没有死信队列时,消息过期了但没来得及消费就会被删除。队列设置过期时间,队列中所有的消息过期时间相同。

2.2 发消息时消息自己设置过期时间

1)消费者发送 10 条消息,且每条设置过期时间为10秒,消费者 10 秒消费一条消息:

可以看出,消费者才消费了两个消息,其他的过期后都消失了。

2)生产者发送 1 条消息,请设置超时时间为 10 秒,消费者接收信息,且不确认:

public class Send {
    private static final String EXCHANGE_NAME = "testMaterialExchange1";
    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"topic",true);

        AMQP.BasicProperties.Builder bd = new AMQP.BasicProperties().builder();
        bd.deliveryMode(2);//持久化  1 是不持久化
        bd.expiration("10000");//设置消息有效期10秒钟

        AMQP.BasicProperties pros = bd.build();

        String message = "测试ttl消息";

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true,false, pros, message.getBytes());

        System.out.println("----send"+message);

        channel.close();
        connection.close();
    }
}
public class Receive2 {
    private static final String EXCHANGE_NAME = "testMaterialExchange1";
    private static final String QUEUE_NAME = "testMaterialGroup3";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);

        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"goods.new.*");

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[2]: " + msg);
                try {
                    Thread.sleep(12000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
//                    channel.basicAck(envelope.getDeliveryTag(),false);

                    System.out.println("[2] done");

                }
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME,autoAck,consumer);

    }
}

可以看到消息一直都被阻塞了:

结论:消息设置的过期时间是指消费者未拿到消息之前,如果消费者还没拿到数据,那么超过设置的有效期并且队列没有设置死信交换机的话,消息就会被删除。如果消费者已经拿到了消息,并且未确认,那么消息就会一直阻塞,不会消失。(无论是消息队列整体设置过期时间还是消息单独设置过期时间都是这样)

2.3 生产者发送 10 条消息,并设置 5 秒过期时间。两个消费者加上  channel.basicQos(1)  方法,设置手动确认且不确认。

生产者:

发送 10 条消息,并设置 5 秒过期时间

public class Send {
    private static final String EXCHANGE_NAME = "testTopicExchange";

    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);

        AMQP.BasicProperties.Builder bd = new AMQP.BasicProperties().builder();
        bd.deliveryMode(2);//持久化
        bd.expiration("5000");//设置消息有效期5秒钟

        AMQP.BasicProperties pros = bd.build();

        for (int i = 0; i < 10; i++) {
            String msg = "hello mq" + i;
            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, pros, msg.getBytes());

            System.out.println("生产者发送消息:" + msg);
        }

        channel.close();
        connection.close();
    }
}

消费者1:

public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[1]: " + msg);

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
//                    channel.basicAck(envelope.getDeliveryTag(),false);
//                    System.out.println("[1] done");
                }

            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

消费者2:

public class Receive2 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String DLX_EXCHANGE_NAME = "dlxExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

//        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明死信交换机(fanout 类型)以及死信队列
//        channel.exchangeDeclare(DLX_EXCHANGE_NAME,"fanout",true,true,null); //声明死信交换机

//        Map<String, Object> arguments = new HashMap<>();
//        arguments.put("x-message-ttl" , 5000);//设置消息有效期1秒,过期后变成私信消息,然后进入DLX  (过期时间在生产者发送消息的时候设置了,这里不重复设置)
//        arguments.put("x-dead-letter-exchange" , DLX_EXCHANGE_NAME);//设置DLX
//        arguments.put("x-dead-letter-routing-key" , "");//设置DLX的路由键(这里的死信交换机是fanout类型,所以这里不设置)

        //声明普通队列 并添加 DLX
//        channel.queueDeclare(QUEUE_NAME,true,false,false,arguments);

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"goods.new.*"); //普通队列绑定生产者的交换机

        channel.basicQos(1); //如果要确认,生产者和消费者都要有这行代码

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 2 接收消息]: " + msg);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {

//                    channel.basicAck(envelope.getDeliveryTag(),false);
//                    System.out.println("[消费者 2 ] done");

                }
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME,autoAck,consumer);

    }
}

消费者 1 接收到了第一条消息,消费者 2 接收到了第二条消息,开始阻塞。5秒后队列中的消息过期消失了。

结论:(channel.basicQos(1)  方法在消费者端写)如果两个消费者绑定了同一个队列,且消费端都设置了 channel.basicQos(1) 方法,设置消息手动确认且不确认,那么第一个消费者拿到第一条数据后这条消息就会阻塞,其他的消费者就会跳开刚刚阻塞的消息拿下一条数据。

2)生产者发送 10 条消息,并设置 5 秒过期时间。两个消费者去掉  channel.basicQos(1)  方法,设置手动确认且不确认。(代码和上面一样,就是去掉了 channel.basicQos(1) )

两个消费者轮询收到了消息,但是队列中的消息没有删掉。

结论:两个消费者连接同一个队列,如果消费端没有加 channel.basicQos(n) 方法,消息默认轮询发送。且如果消息一直没有确认,队列中的消息不会删除。(可以看出如果消息消费者已经拿到了,就不会过期了)

channel.basicQos(5); //每次分发5个需要确认

把生产者和两个消费者的 basicQos 方法改为每次发 5 个消息,两个消费者都设置了手动确认,消费者 1 正常确认,消费者 2 不确认:(这个方法其实在生产端不用写也行)

可以看出,如果 channel.basicQos(5) 写的是5,这说明每发 5 个消息需要确认,否则就不会再发给这个消费者了。上述例子消费者 2 一直未确认,所以收到了 5 条消息之后就阻塞了。还可以看出,其中几条消息阻塞是不会影响队列中后面的消息被消费的(所以消费者1 拿到了被阻塞消息后面的消息),只会影响被阻塞的这个消费者以及这几条消息。(而且前 10 条消息都是轮询发送)

(以及消费者报错都会重试以及报错)

3. 死信队列

消费者发送消息时设置 5 秒过期时间:

import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Description 生产者 交换机为topic类型
 * @Date 2021/4/27
 * @Author Janet
 */
public class Send {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"topic",true);

        AMQP.BasicProperties.Builder bd = new AMQP.BasicProperties().builder();
        bd.deliveryMode(2);//持久化
        bd.expiration("5000");//设置消息有效期5秒钟

        AMQP.BasicProperties pros = bd.build();

        for(int i = 0; i <10; i++){
            String msg = "hello mq"+i;
            System.out.println("生产者发送消息:"+msg);

            //4. 发送消息
            channel.basicPublish(EXCHANGE_NAME,"goods.new.add",true,false, pros, msg.getBytes());

            System.out.println("----send"+msg);

        }

        channel.close();
        connection.close();
    }
}

消费者接收生产者的消息,并消费一次休眠 5 秒钟。生产者的队列设置死信交换机。(所以这个消费者只能正常消费 2 条消息,其他的全部过期进死信队列了)

public class Receive2 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String DLX_EXCHANGE_NAME = "dlxExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明死信交换机(fanout 类型)
        channel.exchangeDeclare(DLX_EXCHANGE_NAME,"fanout",true,true,null); //声明死信交换机

        Map<String, Object> arguments = new HashMap<>();
//        arguments.put("x-message-ttl" , 5000);//设置消息有效期1秒,过期后变成私信消息,然后进入DLX  (过期时间在生产者发送消息的时候设置了,这里不重复设置)
        arguments.put("x-dead-letter-exchange" , DLX_EXCHANGE_NAME);//设置DLX
//        arguments.put("x-dead-letter-routing-key" , "");//设置DLX的路由键(这里的死信交换机是fanout类型,所以这里不设置)

        //声明普通队列 并添加 DLX
        channel.queueDeclare(QUEUE_NAME,true,false,false,arguments);

        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"goods.new.*"); //普通队列绑定生产者的交换机

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[2]: " + msg);
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {

                    channel.basicAck(envelope.getDeliveryTag(),false);
                    System.out.println("[2] done");

                }
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME,autoAck,consumer);

    }
}

再定义一个消费者消费死信队列中的消息:

import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.*;
import java.io.IOException;

/**
 * @Description 定义一个消费者消费死信队列中的消息
 */
public class Receive3 {
    private static final String EXCHANGE_NAME = "dlxExchange"; //死信交换机
    private static final String QUEUE_NAME = "dexGroup"; //死信队列

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, true, false, false, null); //声明死信队列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "null"); //死信队列和死信交换机绑定

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("消费者3接收到死信队列中的消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[消费者3] done");
            }

        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

结论:如果设置了死信队列,过期的消息会进入死信队列重新消费。

4. 拒绝接收数据

/*
         * 拒绝接收消息
         * 参数1:deliveryTag 该消息的index
         * 参数2:multiple:是否批量。true:将一次性拒绝所有小于 deliveryTag 的消息。
         * 参数3:requeue:被拒绝的消息是否重新入队列
         */

void basicNack(long deliveryTag, boolean multiple, boolean requeue)

1)生产者发送 1 条消息,消费者拒绝接收该数据(第三个参数 requeue 设置为 false)且不设置死信队列

消费者:

public class Send {
    private static final String EXCHANGE_NAME = "testTopicExchange";

    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);

        String msg = "hello mq";
        // 发送消息
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, msg.getBytes());

        System.out.println("生产者发送消息:" + msg);

        channel.close();
        connection.close();
    }
}
public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicNack(envelope.getDeliveryTag(), false, false);
                System.out.println("[消费者 1 ]拒绝消息:");


            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

消息被消费者拒绝,且队列中没有消息。

结论:如果没有设置死信队列,消息被拒绝且不重新入队的话消息会被队列删除。

2)生产者发送 1 条消息,消费者拒绝接收该数据(第三个参数 requeue 设置为 false)并设置死信队列。

生产者代码没变。

消费者:

public class Receive2 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String DLX_EXCHANGE_NAME = "dlxExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明死信交换机(fanout 类型)以及死信队列
        channel.exchangeDeclare(DLX_EXCHANGE_NAME, "fanout", true, true, null); //声明死信交换机

        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);//设置DLX

        //声明普通队列 并添加 DLX
        channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.new.*"); //普通队列绑定生产者的交换机

        channel.basicQos(1); //如果要确认,生产者和消费者都要有这行代码

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 2 接收消息]: " + msg);

                channel.basicNack(envelope.getDeliveryTag(), false, false);

                System.out.println("[消费者 1 ]拒绝消息:");

            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

定义消费者接收死信队列中的消息:

public class Receive3 {
    private static final String EXCHANGE_NAME = "dlxExchange"; //死信交换机
    private static final String QUEUE_NAME = "dexGroup"; //死信队列

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, true, false, false, null); //声明死信队列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "null"); //死信队列和死信交换机绑定

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("消费者 3 接收到死信队列中的消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[消费者 3 ] 确认消费消息");
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

结论:如果设置了死信队列,消息被拒绝且不重新入队的话消息会进入死信队列。

3)生产者发送多条消息,两个消费者绑定同一队列。消费者1正常接收数据,消费者2拒绝接收数据(第三个参数 requeue 设置为 true,再入队列)并设置死信队列。

public class Send {
    private static final String EXCHANGE_NAME = "testTopicExchange";

    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);

        for (int i = 0; i < 10; i++) {
            String msg = "hello mq" + i;

            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, msg.getBytes());

            System.out.println("生产者发送消息:" + msg);
        }

        channel.close();
        connection.close();
    }
}
public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

//        HashMap<String, Object> argss = new HashMap<>();
//        argss.put("x-message-ttl", 10000); //设置队列里面的消息过期时间10秒
        //        argss.put("x-max-length",5); //队列的CAHNG

//        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
//                channel.basicNack(envelope.getDeliveryTag(), false, false);
                System.out.println("[消费者 1 ]处理完成:");


            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}
public class Receive2 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String DLX_EXCHANGE_NAME = "dlxExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

//        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明死信交换机(fanout 类型)以及死信队列
//        channel.exchangeDeclare(DLX_EXCHANGE_NAME, "fanout", true, true, null); //声明死信交换机

//        Map<String, Object> arguments = new HashMap<>();
//        arguments.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);//设置DLX

        //声明普通队列 并添加 DLX
//        channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);

//        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.new.*"); //普通队列绑定生产者的交换机

        channel.basicQos(1); //如果要确认,生产者和消费者都要有这行代码

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 2 接收消息]: " + msg);

                //拒绝接收消息,且重新入队列
                channel.basicNack(envelope.getDeliveryTag(), false, true);

//                    channel.basicAck(envelope.getDeliveryTag(),false);
                System.out.println("[消费者 2 ]拒绝消息:");

            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

可以看到,消费者1正常消费数据,消费者 2 一直循环拒绝消息 1 (并且把队列入队了),直到消费者1成功消费完队列中的消息。

4)把上一个情况的 channel.basicQos(1) 改为 channel.basicQos(4),其他情况不变

可以看到,消费者1和消费者2轮询获取 4 条消息,消费者1消费完成之后,继续消费下面的数据,而消费者2由于没有确认,一直在拒绝,所以得到的还是刚刚的4条数据,一直循环拒绝(消费者1每消费完1条,就少循环一条),直到消费者1消费完了队列中的所有消息。

5)以上代码不变,消费者 2 加上 3 秒的休眠时间

可以看到消费者1 消费消息的顺序没有改变,只是消费者2循环的次数减少了。

可以看到消费者根据 channel.basicQos(N) 方法轮询先拿到各自的 N 条数据,然后消费者 1 先消费自己刚刚拿到的数据,再消费没消费过的数据,最后再消费 消费者 2 拒绝过的数据;

消费者 2 一直循环拒绝刚刚未确认的 N 条数据,消费者 1 每消费 1 条,消费者 2 则少循环 1 条。一直到队列被消费者 1 消费完。(消费者 1 消费 消费者 2 拒绝过的数据的顺序,有待考究)

(这里看到,绑定同一个队列的几个消费者,拿到的数据都是顺序轮询的,那要是设置了basicQos(4)方法,且两个消费者都是正常确认,前4个消息也是顺序轮询吗?)

5. 延迟队列

Spring Cloud Stream 进阶配置——使用延迟队列实现“定时关闭超时未支付订单”_多隆的博客-CSDN博客_springcloud stream延迟队列

6. 队列声明后,参数是否可以修改

我先声明一个队列:channel.queueDeclare(QUEUE_NAME, false, false, false, null);

可以看到队列已经生成了,此时我企图重新声明这个队列来改变可持久性:channel.queueDeclare(QUEUE_NAME, true, false, false, null);

但是会报错:

Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'testTopicGroup1' in vhost '/vhost_lyd': received 'true' but current is 'false', class-id=50, method-id=10)

结论:队列一旦声明,参数将无法更改、添加、删除。要改变一个队列的参数,只有两种办法:

  • 删除该队列,重新创建
  • 换个名字,创建一个新的队列

7. 队列的优先级

1)生产者发送 10 条消息,消息优先级为 i ,消费者 1 的队列设置优先级为 5,消费者 2 的队列不设置优先级

/**
 * @Description 生产者  测试队列的优先级、topic交换机
 */
public class Send {
    private static final String EXCHANGE_NAME = "testTopicPriorityExchange";

    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);

        for (int i = 1; i <= 10; i++) {

            AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties().builder();
            builder.priority(i);
            AMQP.BasicProperties pros = builder.build();

            String msg = "hello mq" + i;
            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, pros, msg.getBytes());

            System.out.println("生产者发送消息:" + msg + ",优先级为:"+i);
        }

        channel.close();
        connection.close();
    }
}
import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.HashMap;

public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicPriorityExchange";
    private static final String QUEUE_NAME = "testTopicPriorityGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-priority", 5); //设置队列优先级
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[消费者 1 ]处理完成");
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}
public class Receive2 {
    private static final String EXCHANGE_NAME = "testTopicPriorityExchange";
    private static final String QUEUE_NAME = "testTopicPriorityGroup2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.new.*");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 2 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[消费者 2 ]处理完成");
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

生产者发送 10 条消息,消息优先级为 i ,消费者 1 的队列设置最大优先级为 5,消费者 2 的队列不设置优先级 ,先启动消费者,再启动生产者:

可以看到,消费者 1 除了第 1 条消息以外(一会再验证这一条),5-10 条消息先消费,5 以内的消息按照我们刚刚设置的优先级消费。所以我们猜测如果消息的优先级 > 队列的最大优先级 ,那么这些消息的优先级就不起效果了,所以 5-10条消息按顺序先消费了(至于为什么是5-10条数据先消费,而不是2-4条消息先消费,这里我猜测可能是原来消息设置的优先级就比较大,虽然这几个不按优先级排序了,但还是在 2-4 条消息的优先级之前,所以先消费了),最后才按优先级消费队列最大优先级 5 以内的 3 条数据。

2)设置消费者 1 的队列优先级为 20,消费者 2 消费 1 条消息后休眠 3 秒,先启动两个消费者,再启动生产者:

消费者1:

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-priority", 20); //设置队列优先级
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

消费者2:

Thread.sleep(3000);

可以看到,有优先级的队列消费者1拿到第一条数据后,其余的都是按照优先级消费。消费者2队列没有设置优先级,所以消息也没有按照优先级排序,是按照发消息的顺序消费的。

3)设置消费者 1 的队列优先级为 5,设置每消费一条消息休眠 5 秒,先启动消费者1,再启动生产者:

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-priority", 5); //设置队列优先级
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        ...

        Thread.sleep(3000);
        

4)生产者 5 秒发送一条消息,消费者及时消费消息(消费者的速度 >消费者速度),先启动消费者,再启动生产者:

可以看到消费者按顺序消费消息,说明如果消费者速度 > 生产者速度的话,发来一条就消费一条了,没时间排序。这时设置优先级就没有意义。

5)生产者 5 秒发送一条消息,消费者及时消费消息(消费者的速度 >消费者速度),先启动生产者几秒后,再启动消费者(之前是消费者1启动之前会删除队列的,避免有误差,这里就不要删除队列了,确保生产者发消息的时候,消费者的队列是存在的):

public class Send {
    private static final String EXCHANGE_NAME = "testTopicPriorityExchange";

    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);

        for (int i = 1; i <= 10; i++) {

            AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties().builder();
            builder.priority(i);
            AMQP.BasicProperties pros = builder.build();

            String msg = "hello mq" + i;

            Thread.sleep(3000);

            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, pros, msg.getBytes());

            System.out.println("生产者发送消息:" + msg + ",优先级为:"+i);
        }

        channel.close();
        connection.close();
    }
}
public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicPriorityExchange";
    private static final String QUEUE_NAME = "testTopicPriorityGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

//        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-priority", 5); //设置队列优先级
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[消费者 1 ]处理完成");

            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

先启动生产者之后,发送消息,队列里面已经有数据了,但是消费者还没连接:

生产者 5 秒发送一条消息,消费者及时消费消息(消费者的速度 >消费者速度),先启动生产者几秒后,再启动消费者(之前是消费者1启动之前会删除队列的,避免有误差,这里就不要删除队列了,确保生产者发消息的时候,消费者的队列是存在的)。打开消费者:

发现第 5、6条消息先消费了,再是 4、3、2、1,再是7、8...根据上面的测试,可以推测出:消费者连接队列的时候,队列中已经有了 1-6 条数据,所以前 6 条消息已经排序了(因为这个队列的最大优先级设置了 5,所以第 5 条消息先消费,再根据优先级消费队列最大优先级以内的),后 4 条因为消费者速度 > 生产者速度,所以来一条消费一条,就没有排序。

6)消费者队列最大优先级为 5 ,生产者发消息时,1-4条消息不设置优先级,5-10条消息设置优先级,先启动生产者再启动消费者:

这里可以看出,只要设置了优先级,无论生不生效,都会比没有设置优先级的消息优先执行。

结论:

① 队列可以设置最大优先级N(1-255的整数),消息也可以设置优先级。如果要保证消息按照优先级消费,那么队列和消息都要设置优先级。如果只有消息设置了优先级,而消息进入的队列没有设置最大优先级,那么这些消息设置的优先级就不会起效果,依然还是按照消息进入队列的顺序消费。

② 在队列设置最大优先级的前提下,设置优先级的消息比没有设置优先级的消息先消费。

③ 优先级大的消息先消费。

④ 消息设置的优先级 i 必须小于所在队列的最大优先级 N 才会起效果,否则 i >= N 的消息不会按照优先级排序,还是按照入队列的顺序消费。而 i<N 的消息才会按照优先级消费。并且此时 i >= N 的消息比 i<N 的消息先消费。

⑤ 要想设置的优先级有意义,必须保证生产者的速度 > 消费者的速度,队列中有消息堆积才会排序,不然生产者发来一条就被消费者消费了,队列中至多只有一条消息,根本不会去排序。

8. Max length : 队列可以容纳的消息的最大条数

队列可以容纳的消息的最大条数,超过这个条数,队列头部的消息将会被丢弃。

测试 : 我们设置消费者队列最多只能容纳 1 条消息,生产者一次性发送 10 条消息。

生产者:

        for (int i = 0; i < 10; i++) {
            String msg = "hello mq" + i;
            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, msg.getBytes());

            System.out.println("生产者发送消息:" + msg);
        }

消费者:

public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        HashMap<String, Object> argss = new HashMap<>();
//        argss.put("x-message-ttl", 10000); //设置队列里面的消息过期时间10秒
        argss.put("x-max-length", 1); //队列的长度 设置队列中的消息的最大条数为 1 条,超过1条,则遵循队列的"先进先出(丢)"原则.

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        boolean autoAck = false; //自动应答 false
        //定义一个消费者
        channel.basicConsume(QUEUE_NAME, autoAck, new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);

                System.out.println("[消费者 1 ]处理完成");

            }
        });

    }
}

先启动消费者,再启动生产者:

当生产者发送消息的时候,消费者已经连接队列等待消费了,所以第一条消息进来的时候,队列中有且仅有一条消息,并推给消费者消费。当后面的数据发送之后,由于队列中最多只能有一条消息,所以遵循 “先进先出(丢)” 的原则,消息1-8都被丢弃了,最后剩下消息 9 退给消费者。

所以我们猜测,如果是生产者先发送消息,再启动消费者,消费者只会拿到第 9 条消息,其他的消息会被丢掉。

启动生产者,再启动消费者:

结果确实是这样子的。

结论:如果队列设置了 Max length 为 n,说明队列里面任何时候都有且仅有 n 条数据,且先进先出(丢)。

9. Max length bytes : 队列可以容纳的消息的最大字节数

队列可以容纳的消息的最大字节数,超过这个字节数,队列头部的消息将会被丢弃。

消费者:

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-length-bytes", 10); //设置队列中的消息的最大字节数

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

1)发送一条 18 个字节的消息(我们采用UTF8编码,1个汉字占3个字节),先启动消费者,再启动生产者:

生产者:

            channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
            String msg = "旧故里草木深";
            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, msg.getBytes());

            System.out.println("生产者发送消息:" + msg);

2)发送一条 18 个字节的消息,先启动生产者,再启动消费者(这里的意思是队列还是存在的,只是消费者不在线而已哈):

队列里面直接没有数据,好像是直接扔掉了

3)发送一条 9 个字节的消息。这条和下面的如果没有说明,都是先启动生产者,再启动消费者。

此时这个消息的字节数没有超过 10,所以消息还是在队列里面的。

4)消费者同时发送两条消息,第一条字节数 9,第二条字节数 18。

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草木深".getBytes());
        System.out.println("生产者发送消息:" + "草木深");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧故里草木深".getBytes());
        System.out.println("生产者发送消息:" + "旧故里草木深");

发现队列里面一条数据也没有,好像是直接扔掉了。

5)把上两个消息的顺序换掉

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧故里草木深".getBytes());
        System.out.println("生产者发送消息:" + "旧故里草木深");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草木深".getBytes());
        System.out.println("生产者发送消息:" + "草木深");

发现只有一条数据了,第一条被删掉了。

6)

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草木深".getBytes());
        System.out.println("生产者发送消息:" + "草木深");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧".getBytes());
        System.out.println("生产者发送消息:" + "旧");

7)

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草木".getBytes());
        System.out.println("生产者发送消息:" + "草木深");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧".getBytes());
        System.out.println("生产者发送消息:" + "旧");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧".getBytes());
        System.out.println("生产者发送消息:" + "旧");

8)

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧".getBytes());
        System.out.println("生产者发送消息:" + "旧");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "故".getBytes());
        System.out.println("生产者发送消息:" + "故");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "里".getBytes());
        System.out.println("生产者发送消息:" + "里");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草".getBytes());
        System.out.println("生产者发送消息:" + "草");

9)

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧".getBytes());
        System.out.println("生产者发送消息:" + "旧");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "故".getBytes());
        System.out.println("生产者发送消息:" + "故");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "里".getBytes());
        System.out.println("生产者发送消息:" + "里");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草".getBytes());
        System.out.println("生产者发送消息:" + "草");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "木".getBytes());
        System.out.println("生产者发送消息:" + "深");

10)定义两个消费者,第一个消费者不确认,第二个消费者正常确认消费:

public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-length-bytes", 10); //设置队列中的消息的最大字节数

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        boolean autoAck = false; //自动应答 false
        //定义一个消费者
        channel.basicConsume(QUEUE_NAME, autoAck, new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

//                channel.basicAck(envelope.getDeliveryTag(), false);

//                System.out.println("[消费者 1 ]处理完成");

            }
        });

    }
}
public class Receive2 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String DLX_EXCHANGE_NAME = "dlxExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        boolean autoAck = false; //自动应答 false
        //定义一个消费者
        channel.basicConsume(QUEUE_NAME, autoAck, new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 2 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);

                System.out.println("[消费者 2 ]处理完成");

            }
        });

    }
}

先启动消费者1,再启动生产者发消息,再启动消费者2:

先启动消费者1,再启动消费者2再,启动生产者发消息:

结论:如果设置了 Max length bytes 参数为 n,说明队列里面所有消息的字节数都不会超过 n,遵循先进先出(丢)的原理,而且丢消息是一整条丢。(如果设置了死信队列,那么就进入死信队列)(如果消费者在生产者发消息的时候一直待机,且队列里面已经没有消息了,那么生产者刚发送的消息一下就会发给消费者消费了,设置的队列字节长度就布包括已经发送给消费者的消息,包括未确认的)

10. Overflow behaviour : 队列中的消息溢出后如何处理

队列中的消息溢出时,如何处理这些消息:要么丢弃队列头部的消息(drop-head ),要么丢掉后面生产者发送过来的所有消息(reject-publish)

1)设置队列中最大允许 1 条消息存在,并且丢掉后面生产者发送过来的所有消息(reject-publish)。生产者一次发送 5 条消息。

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧啊啊啊啊".getBytes());
        System.out.println("生产者发送消息:" + "旧啊啊啊啊");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "故".getBytes());
        System.out.println("生产者发送消息:" + "故");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "里".getBytes());
        System.out.println("生产者发送消息:" + "里");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草".getBytes());
        System.out.println("生产者发送消息:" + "草");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "木".getBytes());
        System.out.println("生产者发送消息:" + "木");
public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";
    private static final String DLX_EXCHANGE_NAME = "dlxExchange";
    private static final String DEX_QUEUE_NAME = "dexGroup"; //死信队列

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.exchangeDeclare(DLX_EXCHANGE_NAME, "fanout", true, true, null); //声明死信交换机
        channel.queueDeclare(DEX_QUEUE_NAME, true, false, false, null); //声明死信队列
        channel.queueBind(DEX_QUEUE_NAME, DLX_EXCHANGE_NAME, "null"); //死信队列和死信交换机绑定

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-length", 1); //队列的长度 设置队列中的消息的最大条数为 1 条,超过1条,则遵循队列的"先进先出(丢)"原则.
        argss.put("x-overflow", "reject-publish");  //队列中的消息溢出时,如何处理这些消息:要么丢弃队列头部的消息(drop-head ),要么拒绝接收后面生产者发送过来的所有消息(reject-publish)
//        argss.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);//设置DLX

//        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        boolean autoAck = false; //自动应答 false
        //定义一个消费者
        channel.basicConsume(QUEUE_NAME, autoAck, new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);

                System.out.println("[消费者 1 ]处理完成");

            }
        });

    }
}

消费者消费了第 1 条消息,其他的消息丢掉了了。

2)设置队列中最大允许 1 条消息存在,并且丢弃队列头部的消息(drop-head )。生产者一次发送 5 条消息。

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-length", 1); //队列的长度 设置队列中的消息的最大条数为 1 条,超过1条,则遵循队列的"先进先出(丢)"原则.
        argss.put("x-overflow", "drop-head"); //设置队列中的消息的最大字节数

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

消费者消费了最后 1 条消息,前面的消息丢掉了

结论:队列中的消息溢出时,如果队列设置了 x-overflow 参数,那么值为 drop-head  时会丢弃队列头部的消息,如果值为 reject-publish,就会拒绝后面生产者发送过来的所有消息。

而且,如果此队列设置有死信队列,第一种情况会进入死信队列,第二种不会(下一部分论证)。

11. Dead letter exchange 死信交换机

死信交换机前面已经讲很多了,这里就直接举个例子。

在前一部分中,我们给队列设置了 “x-overflow”的值,丢弃队列头部的消息(drop-head ),或者丢掉后面生产者发送过来的所有消息(reject-publish)

1)当 argss.put("x-overflow", "drop-head"); 时,设置死信交换机

        HashMap<String, Object> argss = new HashMap<>();
        argss.put("x-max-length", 1); //队列的长度 设置队列中的消息的最大条数为 1 条,超过1条,则遵循队列的"先进先出(丢)"原则.
        argss.put("x-overflow", "drop-head"); //设置队列中的消息的最大字节数
        argss.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);//设置DLX

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);
public class Receive3 {
    private static final String DEX_QUEUE_NAME = "dexGroup"; //死信队列

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("消费者 3 接收到死信队列中的消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[消费者 3 ] 确认消费消息");
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(DEX_QUEUE_NAME, autoAck, consumer);

    }
}

看队列中如果消息条数超过了设置的 1,那么扔掉的数据是否会进死信队列:

说明前面扔掉的消息确实进了死信队列。

2)当 argss.put("x-overflow", "reject-publish"); 时,设置死信交换机

argss.put("x-overflow", "reject-publish"); 

嗯,如果是设置了 “reject-publish”,消息就直接扔掉了,就算有死信队列也不会进入死信队列。

12. 消息的持久化

换器的持久化、队列的持久化和消息的持久化。

1)换器的持久化

在声明交换机时 durable 参数置为 true 实现的。如果交换器不设置持久化,那么在 RabbitMQ 服务重启之后,相关的交换器元数据会丢失。

这里我声明一个交换机,durable 参数置为 false:

channel.exchangeDeclare(EXCHANGE_NAME, "topic", false);

在重启 RabbitMQ 时,交换机消失了:

//重启步骤:
rabbitmqctl stop :停止rabbitmq
rabbitmq-server restart : 重启rabbitmq

2)队列的持久化

在声明队列时将 durable 参数置为 true 实现的。如果队列不设置持久化,那么在 RabbitMQ 服务重启之后,相关队列的元数据会丢失,此时数据也会丢失。正所谓“皮之不存,毛将焉附”,队列都没有了,消息又能存在哪里呢? 

声明一个队列:

channel.queueDeclare(QUEUE_NAME, false, false, false, null);

在重启 RabbitMQ 时,队列消失了:

队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。通过将消息的投递模式(BasicProperties 中的 deliveryMode 属性)设置为 2 即可实现消息的持久化


关联数据库:GitHub - skonline/canal-client: spring boot canal starter 易用的canal 客户端 canal client

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值