RabbitMq原生接口详解

文章目录

helloword

生产者

代码

java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
  
import java.nio.charset.StandardCharsets;  
  
public class SingleProducer {  
    private final static String QUEUE_NAME = "hello";  
    public static void main(String[] args) throws Exception {  
        // 创建连接工厂  
        ConnectionFactory factory = new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setUsername("admin");  
        factory.setPassword("admin");  
  
        try(Connection connection=factory.newConnection(); Channel channel=connection.createChannel()){  
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);  
            String message = "Hello World!";  
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));  
            System.out.println(" [x] Sent '" + message + "'");  
        }  
    }  
}

首先我们需要使用ConnectionFactory设置登录账户和密码以及端口号等。

Connection这里可以理解为数据库ConnectionFactory类负责登录mq的管理窗口想要对队列进行操作则需要建立连接然后再获取channel对象进行操作

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

关于queueDeclare方法的各个参数解析

queueDeclare通常用于声明一个队列以下是各个参数的含义

参数 含义
queueName 表示队列的名称是在操作队列时的唯一操作符可以让服务器找到想要操作的队列
durable 表示是否愿意让这个队列持久化即当重启服务器后该队列是否丢失如果设置为true后则说明在后续如果遇到服务器故障当服务器重启完成后该队列以及队列中的数据可以自动恢复
exclusive 当其设置为true后说明队列具有排他性,因此当前队列只有当前连接的人可见并且当连接断开后队列也会进行销毁和回收当其为false后则其可以被多个消费者使用
autoDelete 这里是指该队列是否需要自动销毁回收
arguments 这里是一个map指的是是否需要传递进一些其他的属性

消费者

代码

java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.DeliverCallback;  
  
import java.nio.charset.StandardCharsets;  
  
public class SingleConsumer {  
    private final static String QUEUE_NAME = "hello";  
  
    public static void main(String[] args) throws Exception{  
        // 创建一个 ConnectionFactory 对象,用于创建与消息代理的连接  
        ConnectionFactory factory = new ConnectionFactory();  
        // 设置消息代理的主机地址为 localhost        factory.setHost("localhost");  
        factory.setUsername("admin");  
        factory.setPassword("admin");  
        // 通过 ConnectionFactory 创建一个新的连接  
        Connection connection = factory.newConnection();  
        // 通过连接创建一个通道,通道是执行大多数 API 的地方,例如声明队列、发布消息、消费消息等  
        Channel channel = connection.createChannel();  
        // 声明一个队列,使用 queueDeclare 方法  
        // 参数说明:  
        // QUEUE_NAME:队列的名称  
        // false:表示该队列是否持久化,false 表示不持久化,即如果 RabbitMQ 服务重启,该队列将被删除  
        // false:表示该队列是否排他性,false 表示不排他,多个消费者可以连接到该队列  
        // false:表示该队列是否自动删除,false 表示不会自动删除,即使没有消费者,该队列也会保留  
        // null:表示该队列的其他属性,这里设置为 null 表示没有额外的属性  
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);  
        // 输出等待消息的信息,提示用户按 CTRL+C 退出  
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");  
        // 定义一个 DeliverCallback 对象,用于处理接收到的消息  
        DeliverCallback deliverCallback=(consumerTag, delivery)->{  
            // 将接收到的消息体(byte[] 类型)转换为 UTF-8 编码的字符串  
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);  
            // 打印接收到的消息  
            System.out.println(" [x] Received '" + message + "'");  
        };  
        // 开始消费消息,会持续阻塞等待消息  
        // 参数说明:  
        // QUEUE_NAME:要消费的队列名称  
        // true:表示是否自动确认消息,true 表示一旦消息被发送给消费者,RabbitMQ 就会将其标记为已确认并从队列中删除  
        // deliverCallback:处理接收到消息的回调函数  
        // consumerTag -> { }:取消消费的回调函数,这里是一个空的实现  
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });  
    }  
}

这里前半部分和生产者是一样的操作而basicConsume方法我们进行详细解析

basicConsume

这个方法是一个开始消费信息即从队列中开始获取信息的一个方法。其参数含义如下

参数 含义
queuename 要读取信息的队列名称
ack 这里是一个boolean类型的参数表示的是是否开启自动确认应答,即如果设置为true消费者接收到消息后会自动确认无需手动调用channel.basicAck方法。
DeliverCallback 这里是创建的一个回调函数如果有消息传递给消费者后这个
consumerTag -> { } 指的是取消订阅这个消息队列后调用的方法

workqueue

生产者

代码

java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.MessageProperties;  
  
import java.util.Scanner;  
  
public class MultiProducer {  
    private static final String TASK_QUEUE_NAME = "multi_queue";  
  
    public static void main(String[] args) throws Exception{  
        ConnectionFactory connectionFactory=new ConnectionFactory();  
        connectionFactory.setHost("localhost");  
        connectionFactory.setUsername("admin");  
        connectionFactory.setPassword("admin");  
        try (Connection connection = connectionFactory.newConnection();  
             Channel channel = connection.createChannel()){  
             channel.queueDeclare(TASK_QUEUE_NAME,true,false,false,null);  
             Scanner scanner=new Scanner(System.in);  
             while(scanner.hasNext()){  
                 String message=scanner.nextLine();  
                 channel.basicPublish("",TASK_QUEUE_NAME,  
                         MessageProperties.PERSISTENT_TEXT_PLAIN,  
                         message.getBytes("UTF-8"));  
                 System.out.println(" [x] Sent '" + message + "'");  
             }  
        }  
    }  
}

以上代码和第一个helloword代码的主要区别就是basicPublish方法的运用这里我们解释一下各个参数的含义

basicPublish参数解析

参数 含义
exchange 这里表示交换机的名称我们后面会讲到这里设置为空表示使用默认的交换机
TASK_QUEUE_NAME(routingKey内部源码有一个处理我们下面来说这里简单理解为队列名称) 表示队列名称
MessageProperties.PERSISTENT_TEXT_PLAIN(props) 这个参数表示的消息的属性设置此处的参数含义是该消息为持久化消息,即当服务器重启后该消息不会丢失
message.getBytes("UTF-8")(body) 这个是body参数是消息的主体内容一般我们会将其设置为utf8
该方法的主要目的是为了将消息传递给消息队列其内部的参数则是设置了该消息传递应当如何传递,是否持久是否使用创建的交换机等等

消费者

代码

java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.DeliverCallback;  
  
public class MultiConsumer {  
    private static final String TASK_QUEUE_NAME = "multi_queue";  
  
    public static void main(String[] args) throws Exception{  
        ConnectionFactory connectionFactory=new ConnectionFactory();  
        connectionFactory.setHost("localhost");  
        connectionFactory.setUsername("admin");  
        connectionFactory.setPassword("admin");  
        final Connection connection = connectionFactory.newConnection();  
        for(int i=0;i<2;i++){  
            final Channel channel = connection.createChannel();  
            channel.queueDeclare(TASK_QUEUE_NAME,true,false,false,null);  
            System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); 
            channel.basicQos(1);  
            int finalI = i;  
            DeliverCallback deliverCallback=(consumerTag, delivery)->{  
                String message=new String(delivery.getBody(), "UTF-8");  
                try {  
                    System.out.println(" [x] Received '" + "编号:" + finalI + ":" + message + "'");  
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);  
                    //停20秒  
                    Thread.sleep(20000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }finally {  
                    System.out.println(" [x] Done");  
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);  
                }  
            };  
            // 开启消费监听  
            channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {  
            });  
        }  
    }  
}

代码解析

channel.basicQos(1);

它允许消费者控制预取消息的数量,防止消费者接收过多消息而无法及时处理,导致消息积压或系统过载。这里涉及到了rabbitmq存取消息的一个过程我们可以进行详细的讲解一下

rabbitMq存取消息流程

当我们启动rabbitMq服务器之后操作系统会给rabbitMq划分一块内存作为mq存储队列,消息,连接器等
basicPublish 当我们调用该方法后我们的消息会被发送给rabbitmq服务器,服务器再根据我们的消息的属性例如交换器等将该消息存储到某个队列中
basicConsume 当我们调用该方法后那么消费者就会与rabbitmq服务器建立起来连接并订阅相应的消息队列当该队列有消息的时候RabbitMQ 服务器会将消息传递给消费者

交换器(Publish/Subscribe)

这里官方文档给的一个归栏是发布者和订阅者,其实指的是交换器和队列之间的关系这里我们将从以下交换器进行介绍

fanout交换器

fanout交换器是交换器的一种类型我们来看下官网的解析
The core idea in the messaging model in RabbitMQ is that the producer never sends any messages directly to a queue. Actually, quite often the producer doesn't even know if a message will be delivered to any queue at all.
Instead, the producer can only send messages to an _exchange_. An exchange is a very simple thing. On one side it receives messages from producers and the other side it pushes them to queues. The exchange must know exactly what to do with a message it receives. Should it be appended to a particular queue? Should it be appended to many queues? Or should it get discarded. The rules for that are defined by the _exchange type_.

我们对以上进行翻译
RabbitMQ 消息传递模型的核心思想是,生产者从不直接将任何消息发送到队列。实际上,生产者常常甚至不知道消息是否会被投递到哪个队列。
相反,生产者只能将消息发送到一个交换器。交换器是一种非常简单的组件。它一端接收来自生产者的消息,另一端将消息推送到队列。交换器必须确切知道如何处理接收到的消息。是将消息附加到特定的某个队列?还是附加到多个队列?亦或是将其丢弃。这些规则由交换器类型来定义。

这里的意思就是在最开始我们的消息传递时生产者直接将消息发送给队列现在变成了生产者将消息发给交换器由交换器将其发送给不同的队列(以fanout为例)

上图其实时fanout交换器在接收到消息后如何发送我们可以看到fanout交换器是将消息统一发给与其绑定的全部队列那么代码层我们来看一下

生产者

代码
java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
  
import java.util.Scanner;  
  
public class FanoutProducer {  
    private static final String EXCHANGE_NAME = "fanout-exchange";  
  
    public static void main(String[] args) throws Exception {  
        ConnectionFactory factory=new ConnectionFactory();  
        factory.setPassword("admin");  
        factory.setUsername("admin");  
        factory.setHost("localhost");  
        try(Connection connection=factory.newConnection();  
            Channel channel=connection.createChannel()){  
            channel.exchangeDeclare(EXCHANGE_NAME,"fanout");  
            Scanner scanner=new Scanner(System.in);  
            while(scanner.hasNext()){  
                String message=scanner.nextLine();  
                channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("utf-8"));  
                System.out.println(" [x] Sent '" + message + "'");  
            }  
        }  
  
    }  
}
代码解析
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");

这行代码的作用其实就是声明一个交换器,我们可以类比上面的生产者进行对比

参数 含义
EXCHANGE_NAME 交换器名称
type(fanout) 交换器的类型、

消费者

代码
java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.DeliverCallback;  
  
public class FanoutConsumer {  
    private static final String EXCHANGE_NAME = "fanout-exchange";  
  
    public static void main(String[] args) throws Exception {  
        ConnectionFactory factory=new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setUsername("admin");  
        factory.setPassword("admin");  
        Connection connection=factory.newConnection();  
        Channel channel1=connection.createChannel();  
        Channel channel2=connection.createChannel();  
        channel1.exchangeDeclare(EXCHANGE_NAME, "fanout");  
        String queueName = "xiaowang_queue";  
        channel1.queueDeclare(queueName,true,false,false,null);  
        channel1.queueBind(queueName, EXCHANGE_NAME, "");  
        String queueName2 = "xiaoli_queue";  
        channel1.queueDeclare(queueName2,true,false,false,null);  
        channel1.queueBind(queueName2, EXCHANGE_NAME, "");  
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");  
        DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {  
            String message = new String(delivery.getBody(), "UTF-8");  
            System.out.println(" [小王] Received '" + message + "'");  
        };  
  
        DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {  
            String message = new String(delivery.getBody(), "UTF-8");  
            System.out.println(" [小李] Received '" + message + "'");  
        };  
        channel1.basicConsume(queueName,true,deliverCallback1,consumerTag->{});  
        channel2.basicConsume(queueName2,true,deliverCallback2,consumerTag->{});  
    }  
}
代码解析
channel1.queueBind(queueName, EXCHANGE_NAME, "");
参数 含义
queueName 绑定交换器的队列名
exchangeName 绑定的哪个交换器名称
routingKey("") 路由键。它是一个字符串,当交换器将消息路由到队列时会使用这个路由键。不同类型的交换器对路由键的使用方式不同接下来使用direct将会使用

该方法的作用就是可以让该队列绑定该交换机从而使得交换机可以向绑定自己的队列发送消息

direct交换机

作用

在上述fanout的解释中我们可以知道随着业务复杂度的提升单纯的将消息传递给所有的消息队列已经无法满足我们的业务需求了因此我们需要一个交换器可以帮助我们将特定的消息发送给特定的队列即针对消息进行一个分类管理而我们的direct便有这个功能他的传递消息图如下.

可以实现的功能

通过这样的特性我们可以对消息实现一个分类比如说,我们在注册一个QQ账号的时候如果是fanout路由那么我们所有的消息都是发送给所有的队列,包括注册失败,注册成功等等消息这很明显不符合我们的项目要求,因此我们可以对消息分类,成功的放入一个队列失败的放入一个队列从而实现了降低耦合提高内聚

生产者

代码
java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
  
import java.util.Scanner;  
  
public class DirectProducer {  
    private static final String EXCHANGE_NAME = "direct-exchange";  
  
    public static void main(String[] args) throws Exception{  
        ConnectionFactory factory=new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setUsername("admin");  
        factory.setPassword("admin");  
        try (Connection connection = factory.newConnection();  
             Channel channel = connection.createChannel()) {  
            channel.exchangeDeclare(EXCHANGE_NAME, "direct");  
            Scanner scanner = new Scanner(System.in);  
            while (scanner.hasNext()) {  
                String userInput = scanner.nextLine();//输入一个字符串前半部分是message后半部分是routingKey中间是空格  
                String[] strings = userInput.split(" ");//以空格分割  
                if (strings.length < 1) {  
                    continue;  
                }  
                String message = strings[0];  
                String routingKey = strings[1];  
                channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));  
                System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");  
            }  
  
        }  
  
    }  
}
代码解析
basicPublish填坑

![[Pasted image 20250118134640.png]]

在对上面解析的时候我提到了这个方法的内部参数名称官方起的时routingKey而上面我说先简单理解为传递队列名称,因为其内部源码有这样的一个处理,那就是当我们exchange为空的时候那就默认为使用rabbitMq默认的一个路由交换器,此时routingKey就会被认为是队列名称因此这里看似我们在对exchange传递的是空但是其实本质我们也是调用了rabbitMq的默认路由

那么basicPublish的实际使用其实rottingKey参数传递的是一个路由键,可以理解为就像一个哈希表一样,在消费者中调用queuebind的时候我们有一个参数也就是routingKey请参考上面的fanout消费者代码解析 上面我们提到了在绑定交换机的时候我们可以设置一个routingKey其实可以理解为一个哈希表,routingKey是键然后我们通过这个路由键实现了可以将消息转给特定的消息队列

消费者

代码
java 复制代码
package com.monai.aidati.mq;  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.DeliverCallback;  
  
public class DirectConsumer {  
    private static final String EXCHANGE_NAME = "direct-exchange";  
  
    public static void main(String[] args) throws Exception {  
        ConnectionFactory factory=new ConnectionFactory();  
        factory.setPassword("admin");  
        factory.setUsername("admin");  
        factory.setHost("localhost");  
        Connection connection=factory.newConnection();  
        Channel channel=connection.createChannel();  
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");  
        // 创建队列,随机分配一个队列名称  
        String queueName = "xiaoyu_queue";  
        channel.queueDeclare(queueName, true, false, false, null);  
        channel.queueBind(queueName, EXCHANGE_NAME, "xiaoyu");  
        // 创建队列,随机分配一个队列名称  
        String queueName2 = "xiaopi_queue";  
        channel.queueDeclare(queueName2, true, false, false, null);  
        channel.queueBind(queueName2, EXCHANGE_NAME, "xiaopi");  
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");  
        DeliverCallback xiaoyuDeliverCallback=(consumerTag, delivery)->{  
            String message=new String(delivery.getBody(),"utf-8");  
            System.out.println(" [xiaoyu] Received '" +  
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");  
        };  
        DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {  
            String message = new String(delivery.getBody(), "UTF-8");  
            System.out.println(" [xiaopi] Received '" +  
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");  
        };  
        channel.basicConsume(queueName,true,xiaoyuDeliverCallback,consumerTag->{});  
        channel.basicConsume(queueName2,true,xiaopiDeliverCallback,consumerTag->{});  
    }  
}
代码解析
queueBind

![[Pasted image 20250118141905.png]]

在这段代码中我们可以看到我们的routingKey已经写入了值,也就是说该队列绑定交换机后交换机可以通过这个routingkey向特定的消息队列发送消息

Topic交换机

作用

在上面的交换机我们学到了发送给全部队列,发送给指定队列,但是我们在生活中的实际应用场景其实比上面的还要复杂一些因为我们还需要批量发送消息那么Topic就可以满足我们这个需求

可实现的功能

在这里我们可以实现消息的批量转发比如说在一个公司中老板需要给全体员工发送消息1给全体管理层发送消息2那么可以使用Topic交换机

生产者

代码
java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
  
import java.util.Scanner;  
  
public class TopicProducer {  
    private static final String EXCHANGE_NAME = "topic-exchange";  
  
    public static void main(String[] args) throws Exception {  
        ConnectionFactory factory = new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setUsername("admin");  
        factory.setPassword("admin");  
        try (Connection connection = factory.newConnection();  
             Channel channel = connection.createChannel()) {  
            channel.exchangeDeclare(EXCHANGE_NAME,"topic");  
            Scanner scanner = new Scanner(System.in);  
            while (scanner.hasNext()) {  
                String userInput = scanner.nextLine();  
                String[] strings = userInput.split(" ");  
                if (strings.length < 1) {  
                    continue;  
                }  
                String message = strings[0];  
                String routingKey = strings[1];  
  
                channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));  
                System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");  
            }  
        }  
    }  
}

这里的生产者代码其实和direct的代码是共用的

消费者

代码
java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.DeliverCallback;  
  
public class TopicConsumer {  
    private static final String EXCHANGE_NAME = "topic-exchange";  
  
    public static void main(String[] args) throws Exception {  
        ConnectionFactory factory=new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setUsername("admin");  
        factory.setPassword("admin");  
        Connection connection=factory.newConnection();  
        Channel channel=connection.createChannel();  
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");  
        String queueName = "frontend_queue";  
        channel.queueDeclare(queueName,true,false,false,null);  
        channel.queueBind(queueName,EXCHANGE_NAME,"#.前端.#");  
        // 创建队列  
        String queueName2 = "backend_queue";  
        channel.queueDeclare(queueName2, true, false, false, null);  
        channel.queueBind(queueName2, EXCHANGE_NAME, "#.后端.#");  
  
        // 创建队列  
        String queueName3 = "product_queue";  
        channel.queueDeclare(queueName3, true, false, false, null);  
        channel.queueBind(queueName3, EXCHANGE_NAME, "#.产品.#");  
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");  
        DeliverCallback xiaoaDeliverCallback = (consumerTag, delivery) -> {  
            String message = new String(delivery.getBody(), "UTF-8");  
            System.out.println(" [xiaoa] Received '" +  
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");  
        };  
  
        DeliverCallback xiaobDeliverCallback = (consumerTag, delivery) -> {  
            String message = new String(delivery.getBody(), "UTF-8");  
            System.out.println(" [xiaob] Received '" +  
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");  
        };  
  
        DeliverCallback xiaocDeliverCallback = (consumerTag, delivery) -> {  
            String message = new String(delivery.getBody(), "UTF-8");  
            System.out.println(" [xiaoc] Received '" +  
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");  
        };  
        channel.basicConsume(queueName, true, xiaoaDeliverCallback, consumerTag -> {  
        });  
        channel.basicConsume(queueName2, true, xiaobDeliverCallback, consumerTag -> {  
        });  
        channel.basicConsume(queueName3, true, xiaocDeliverCallback, consumerTag -> {  
        });  
    }  
}
代码解析
channel.queueBind(queueName2, EXCHANGE_NAME, "#.后端.#");

这里我们可以发现我们在设置routingKey的时候用了一种类似于正则表达式的样式但是还是有一定区别的

* . 可以匹配一个单词比如说 * .ori 那么a.ori , b.ori , c.ori都可以匹配

#. 可以匹配0个或者多个单词比如说 a.# 那么a.a a.a.a a.b都可以匹配到

这里我尝试过去掉 . 进行实验但是实验结果显示去掉后匹配就不能成功了可能这是它默认的匹配规则吧。

RPC

除了以上的交换机类型外官方还提供了一个交换机类型就是RPC

RPC 即远程过程调用,是一种允许一个程序调用另一个地址空间(通常是远程计算机)中的过程或函数的技术,而开发人员无需显式编码远程交互的细节。在分布式系统中,RPC 是一种常用的通信模式,它使得开发人员可以像调用本地函数一样调用远程服务,隐藏了网络通信的复杂性。但是的话我们如果需要使用RPC也有专门的RPC框架,就像redis也可以当作消息队列但是我们不会为了模拟而模拟,一般我们都是使用专门的框架技术

核心特性

消息过期机制

在上面讲解basicPublish的各项参数含义时候我们提到了持久化机制,那么我们可以理解就是我们的消息是可以设置过期时间的,其实也很好理解,因为消息队列也好,交换机也好这些占用的都是我们的内存,那么如果不设置一个合理的过期机制就很容易导致我们的消息出现挤压导致内存的浪费。

生产者

代码
java 复制代码
package com.monai.aidati.mq;  
  
import com.rabbitmq.client.AMQP;  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
  
public class TtlProducer {  
    private final static String QUEUE_NAME = "ttl_queue";  
  
    public static void main(String[] args) throws Exception{  
        ConnectionFactory factory=new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setUsername("admin");  
        factory.setPassword("admin");  
        try (Connection connection=factory.newConnection();  
             Channel channel=connection.createChannel()){  
            channel.queueDeclare(QUEUE_NAME,true,false,false,null);  
            String message = "Hello World!";  
            AMQP.BasicProperties basicProperties=new AMQP.BasicProperties().builder()  
                    .expiration("10000")  
                    .build();  
            channel.basicPublish("",QUEUE_NAME,basicProperties,message.getBytes("utf8"));  
        }  
  
    }  
}
代码解析

在以上代码中我们用到了之前没有用到的一个参数basicProperties这里我们之前提出过是为了设置一些消息的属性那么这里我们便可以设置消息的属性之一也就是过期时间为10秒消费者的话其实代码没有做过多的变动可以直接使用helloword的代码调用即可观察现象。

消息确认机制

核心概念

这里我们需要了解生产者发送的消息是如何发送到消费者程序中的如下图

如图所示生产者生产的消息能够被消费者获取到是因为生产者会将生产的消息发送给broker这是一个中间件,它的作用就是将生产者生产出的消息传递给消费者。而broker在获取到消息之后会根据这个消息的各种配置选择合适的路由等,并且当收到消息后会给生产者发送一个ack表示成功接收,并把该消息发送给消费者,那么在这个过程中有哪些地方容易出现异常呢?

异常情况1 生产者生产的消息未能传递给broker

针对生产者和broker之间的消息传递rabbitMq设置了三个样式的消息确认机制

Publishing Messages Individually(单独确认)
java 复制代码
   static void publishMessagesIndividually() throws Exception {  
        try (Connection connection = createConnection()) {  
            Channel ch = connection.createChannel();  
//开启信道确认模式  
            ch.confirmSelect();  
//声明队列  
            ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME1, true, false, true,  
                    null);  
            long start = System.currentTimeMillis();  
//循环发送消息  
            for (int i = 0; i < MESSAGE_COUNT; i++) {  
                String body = "消息"+ i;  
//发布消息  
                ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME1, null,  
                        body.getBytes());  
//等待确认消息.只要消息被确认,这个方法就会被返回  
//如果超时过期, 则抛出TimeoutException。如果任何消息被nack(丢失),  
                waitForConfirmsOrDie将抛出IOException。  
                ch.waitForConfirmsOrDie(5_000);  
            }  
            long end = System.currentTimeMillis();  
            System.out.format("Published %d messages individually in %d ms",  
                    MESSAGE_COUNT, end - start);  
        }  
    }

相较于普通的生产者这里的主要区别就是这里我们新增的代码就是上面这两行那么我们主要讲解waitForConfirmsOrDie

这个方法可以设置一个参数这个参数是broker未返回ack的话我们最多等待多长时间他的底层原理如下

1 生产者向broker发送消息

2 broker收到消息后会按照顺序对收到的消息设置一个id这个id通常是自增的

3 broker收到消息后会自动的为生产者发送一个ack并且rabbitMq内部会维持一张表上面记录着对应id的消息此时是否发送ack报文

4 生产者调用waitForConfirmsOrDie会陷入阻塞等待一直等待到broker发送的ack报文获取到为止

Publishing Messages in Batches(批量确认)
java 复制代码
    static void publishMessagesInBatch() throws Exception {  
        try (Connection connection = createConnection()) {  
//创建信道  
            Channel ch = connection.createChannel();  
//信道设置为confirm模式  
            ch.confirmSelect();  
//声明队列  
            ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME2, true, false, true,  
                    null);  
            int batchSize = 100;  
            int outstandingMessageCount = 0;  
            long start = System.currentTimeMillis();  
            for (int i = 0; i < MESSAGE_COUNT; i++) {  
                String body = "消息"+ i;  
//发送消息  
                ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME2, null,  
                        body.getBytes());  
                outstandingMessageCount++;  
//批量确认消息  
                if (outstandingMessageCount == batchSize) {  
                    ch.waitForConfirmsOrDie(5_000);  
                    outstandingMessageCount = 0;  
                }  
            }  
//消息发送完, 还有未确认的消息, 进行确认  
            if (outstandingMessageCount > 0) {  
                ch.waitForConfirmsOrDie(5_000);  
            }  
            long end = System.currentTimeMillis();  
            System.out.format("Published %d messages in batch in %d ms",  
                    MESSAGE_COUNT, end - start);  
        }  
    }

不同于同步处理机制,批量处理显著的减少了网络通信的负担因为它会设置一个消息量当已发送的消息达到这个量的时候会进行一次确认,这时候就用到了rabbitMq维护的一张broker是否进行ack确认的表了,他会根据以发送消息的id进行排场从而获取到哪些消息还没有进行ack确认。

Handling Publisher Confirms Asynchronously(异步确认)

异步确认的机制是这三者实现最为复杂但也是最为高效的一种确认方式,他的基本原理就是确认和机制和发送消息不是同时进行的这得益于rabbitMq实现了一个方法addConfirmListener这个方法

可以添加ConfirmListener 回调接口.而回调窗口包含了两个方法handleAck(long deliveryTag, boolean

multiple) 和 handleNack(long deliveryTag, boolean multiple),从方法的名称我们可以知道addConfirmListener其实就是添加了一个监听器,而回调接口内实现的两个方法则是当broker返回ack和nack的时候的处理,异步确认需要我们的生产者维护一个表格,也就是把全部的消息放入该表格中,然后当接收到一个ack后把该消息从表格中移除,最终表格中剩余的消息也就是没有接收到ack的消息了

java 复制代码
   static void handlePublishConfirmsAsynchronously() throws Exception {  
        try (Connection connection = createConnection()) {  
            Channel ch = connection.createChannel();  
            ch.queueDeclare(PUBLISHER_CONFIRMS_QUEUE_NAME3, false, false, true,  
                    null);  
            ch.confirmSelect();  
//有序集合,元素按照自然顺序进行排序,存储未confirm消息序号  
            SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new  
                    TreeSet<>());  
            ch.addConfirmListener(new ConfirmListener() {  
                @Override  
                public void handleAck(long deliveryTag, boolean multiple) throws  
                        IOException {  
//System.out.println("ack, SeqNo: " + deliveryTag +  
                    ",multiple:" + multiple);  
//multiple 批量  
//confirmSet.headSet(n)方法返回当前集合中小于n的集合  
                    if (multiple) {  
//批量确认:将集合中小于等于当前序号deliveryTag元素的集合清除,表示  
                        这批序号的消息都已经被ack了  
                        confirmSet.headSet(deliveryTag+1).clear();  
                    } else {  
//单条确认:将当前的deliveryTag从集合中移除  
                        confirmSet.remove(deliveryTag);  
                    }  
                }  
                @Override  
                public void handleNack(long deliveryTag, boolean multiple) throws  
                        IOException {  
                    System.err.format("deliveryTag: %d, multiple: %b%n",  
                            deliveryTag, multiple);  
                    if (multiple) {  
//批量确认:将集合中小于等于当前序号deliveryTag元素的集合清除,表示  
                        这批序号的消息都已经被ack了  
                        confirmSet.headSet(deliveryTag+1).clear();  
                    } else {  
//单条确认:将当前的deliveryTag从集合中移除  
                        confirmSet.remove(deliveryTag);  
                    }  
//如果处理失败, 这里需要添加处理消息重发的场景. 此处代码省略  
                }  
            });  
//循环发送消息  
            long start = System.currentTimeMillis();  
            for (int i = 0; i < MESSAGE_COUNT; i++) {  
                String message = "消息" + i;  
//得到下次发送消息的序号, 从1开始  
                long nextPublishSeqNo = ch.getNextPublishSeqNo();  
//System.out.println("消息序号:"+ nextPublishSeqNo);  
                ch.basicPublish("", PUBLISHER_CONFIRMS_QUEUE_NAME3, null,  
                        message.getBytes());  
//将序号存入集合中  
                confirmSet.add(nextPublishSeqNo);  
            }  
//消息确认完毕  
            while (!confirmSet.isEmpty()){  
                Thread.sleep(10);  
            }  
            long end = System.currentTimeMillis();  
            System.out.format("Published %d messages and handled confirms  
                    asynchronously in %d ms%n", MESSAGE_COUNT, end - start);  
        }  
    }
相关推荐
Cent'Anni27 分钟前
集群、分布式及微服务间的区别与联系
java·分布式
m0_748254881 小时前
【VxLAN】二、VxLAN-EVPN分布式网关-ensp实验
分布式
明达技术1 小时前
科技护航:分布式 IO 模块与大型 PLC,稳筑地铁安全防线
分布式·科技
九河云1 小时前
分布式数据库中间件(DDM)的使用场景
数据库·分布式·中间件·华为云
m0_748234711 小时前
分布式多卡训练(DDP)踩坑
分布式
明达技术1 小时前
分布式 IO 模块:开启药品罐装产线高效生产新纪元
分布式
线程A3 小时前
Kafka 源码分析(一) 日志段
分布式·kafka
S-X-S4 小时前
「2024 博客之星」自研Java框架 Sunrays-Framework 使用教程
java·rabbitmq·springboot·web·log4j2·minio·脚手架
Onlooker1294 小时前
RabbitMQ1-消息队列
rabbitmq