RabbitMQ:高效传递消息的魔法棒,一篇带你助力构建可靠的分布式系统(上篇)_rabbitmq rpc 什么格式 传送最快

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

commons-io commons-io 2.11.0

### 2.2 创建生产者



package com.javadouluo.abbitmq;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
* @author jektong
* @date 2023年05月03日 17:05
*/
public class Producer {

// 队列名称
private static final  String QUEUE_NAME = "hello world";

public static void main(String[] args) throws Exception{
    // 创建一个工厂
    ConnectionFactory connectionFactory = new ConnectionFactory();
    // 工厂IP 连接RabbitMQ队列
    connectionFactory.setHost("192.168.10.100");
    // 用户名
    connectionFactory.setUsername("guest");
    // 密码
    connectionFactory.setPassword("guest");
    // 创建连接
    Connection connection = connectionFactory.newConnection();
    // 获取信道
    Channel channel = connection.createChannel();
    /\*\*

* 生成一个队列
* 1.队列名称
* 2.队列中消息是否持久化,默认消息在内存中
* 3.是否只给一个消费者消费,true消息可以共享,false消息不可共享
* 4.是否自动删除,最后一个消费者断开连接之后是否自动删除该队列 true是自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 发消息
String message = “hello world”;
/**
* 发送一个消费
* 1.发送到哪个交换机
* 2.路由的key值哪个,这次是队列名称
* 3.其他参数信息
* 4.发送消息的消息内容
*/
channel.basicPublish(“”,QUEUE_NAME,null,message.getBytes());
System.out.println(“消息发送完毕!!!”);
}
}


`basicPublish()` 方法是RabbitMQ中AMQP协议提供的方法之一,用于将消息发布到指定的交换机中。


该方法的参数如下:


* `exchange`:表示消息发送到哪个交换机上,可以为空,表示使用默认的交换机。
* `routingKey`:表示路由键,用于指定将消息路由到哪些队列中。如果使用默认的交换机,那么路由键就需要指定为队列名称。
* `props`:表示消息的属性信息,一般为空,使用默认的属性即可。
* `body`:表示要发送的消息内容,需要转换成字节数组形式。


使用 `basicPublish()` 方法可以将消息发送到交换机中,然后由交换机根据路由键将消息路由到对应的队列中,等待消费者进行消费。


执行完成之后,打开MQ管理界面,会发现名称为`hello world`的队列名称。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/11303102205f4c8eaa94dbcc8bb78e03.png#pic_center)  
 主页面也会显示详细的信息:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/e697f2062f95487899670b9239dab28e.png#pic_center)


### 2.3 创建消费者



package com.javadouluo.abbitmq;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
* @author jektong
* @date 2023年05月03日 19:26
*/
public class Consumer {

public static final String QUEUE_NAME = "hello world";

public static void main(String[] args) throws IOException, TimeoutException {
    // 创建一个工厂
    ConnectionFactory connectionFactory = new ConnectionFactory();
    // 工厂IP 连接RabbitMQ队列
    connectionFactory.setHost("192.168.10.100");
    // 用户名
    connectionFactory.setUsername("guest");
    // 密码
    connectionFactory.setPassword("guest");
    // 创建连接
    Connection connection = connectionFactory.newConnection();
    // 创建信道
    Channel channel = connection.createChannel();
    // 声明 接收消息
    DeliverCallback deliverCallback = (consumerTag,message)->{
        System.out.println(new String(message.getBody()));
    };
    // 取消消息时的回调
    CancelCallback cancelCallback = consumerTag->{
        System.out.println("消息消费被中断");
    };
    /\*\*

* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否自动应答true自动应答,false代表手动应答
* 3.消费者未成功消费的回调
* 4.消费者取消消费的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}


`basicConsume` 方法用于开始消费一个队列中的消息。具体来说,它向 RabbitMQ 服务器发送一个指令,告诉它我们希望从指定的队列中获取消息并开始消费。


`basicConsume` 方法的常用参数如下:


* `queue`:需要消费的队列的名称;
* `autoAck`:如果为 `true`,则消费者获取到消息后立即自动确认;如果为 `false`,则需要调用 `basicAck` 方法来手动确认消息;
* `consumerTag`:消费者标识,用于标识当前消费者。**如果不设置该参数,RabbitMQ 会为每个消费者生成一个唯一标识**;
* `deliverCallback`:消息处理回调函数,用于处理队列中获取到的消息。
* `cancelCallback`:取消消费的回调函数,用于在消费者被取消消费时执行。


当一个消费者调用 `basicConsume` 方法后,RabbitMQ 服务器会立即将队列中的消息推送给它,并在消息发送完成后立即进行确认,以便让 RabbitMQ 知道该消息已经被消费过。


如果 `autoAck` 参数为 `false`,则消费者需要调用 `basicAck` 方法来手动确认消息。


如果消费者在处理消息的过程中发生了异常,也可以调用 `basicNack` 方法将消息重新加入队列,以便重新进行消费。


运行代码查看消息已被消费:


![在这里插入图片描述](https://img-blog.csdnimg.cn/754fb17a9a3143d695fab8bd4b4a010a.png#pic_center)


## 三. Work Queues(工作队列模式)


### 3.1 创建工作线程


![在这里插入图片描述](https://img-blog.csdnimg.cn/67b32e7ba9204f0e9c117456a5735e18.png#pic_center)  
 工作队列模式中,生产者发消息到队列,多个消费者从队列中获取消息并进行处理。每个消息只会被一个消费者处理,保证**每个消息只会被处理一次,它们之间是竞争关系**。


这个模式的特点是在消费者之间分配耗时的任务,一旦一个消息被消费者接收,它就会被从队列中删除。


`RabbitMQ`会轮流地将消息发送给每个消费者。当消费者处理较慢或者某个消费者出现宕机等情况时,`RabbitMQ`会重新将消息发送给其他消费者进行处理。


多个消费者,也称之为多个工作线程,下面用代码的方式去完成工作队列模式。


首先我们先改造上面消费者,复制两个工作线程(消费者)代码,为了方便加入一行注释即可,`Work01.java`代码如下:



/**
* 这是一个工作线程
* @author jektong
* @date 2023年05月08日 21:49
*/
public class Work01 {

public static final String QUEUE_NAME = "hello world";

public static void main(String[] args) throws IOException, TimeoutException {
	// 中间省略,与上面的消费者代码一致
    // 接收消息
    /\*\*

* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否自动应答true自动应答,false代表手动应答
* 3.消费者未成功消费的回调
* 4.消费者取消消费的回调
*/
System.out.println(“work01等待接收消息”);
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}


`Work02.java`代码如下:



/**
* 这是一个工作线程
* @author jektong
* @date 2023年05月08日 21:49
*/
public class Work01 {

public static final String QUEUE_NAME = "hello world";

public static void main(String[] args) throws IOException, TimeoutException {
	// 中间省略,与上面的消费者代码一致
    // 接收消息
    /\*\*

* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否自动应答true自动应答,false代表手动应答
* 3.消费者未成功消费的回调
* 4.消费者取消消费的回调
*/
System.out.println(“work02等待接收消息”);
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}


### 3.2 创建生产者


这部分依然是基于上面的生产者代码,主要模拟出发送多个消息即可:



package com.javadouluo.abbitmq.two;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.util.Scanner;

/**
* @author jektong
* @date 2023年05月08日 22:10
* 生产者发送消息
*/
public class Task01 {

// 队列名称
private static final  String QUEUE_NAME = "hello world";

public static void main(String[] args) throws Exception{
     
	// 中间代码省略,与上面一致
	
    /\*\*

* 发送一个消费
* 1.发送到哪个交换机
* 2.路由的key值哪个,这次是队列名称
* 3.其他参数信息
* 4.发送消息的消息内容
*/
// 发送5条消息
for (int i = 0; i < 5; i++) {
channel.basicPublish(“”,QUEUE_NAME,null,(“消息编号” + i).getBytes());
}
System.out.println(“消息发送完毕!!!”);
}
}


### 3.3 结果分析


启动两个工作线程,`work01`在等待消息:


![在这里插入图片描述](https://img-blog.csdnimg.cn/4b1426b474de49df96a1b27d7d30d781.png#pic_center)


`work02`也在等待消息:


![在这里插入图片描述](https://img-blog.csdnimg.cn/6f946b3e5bed4e288ce51348d69475fa.png#pic_center)


好的,现在我们开始启动生产者发送消息给消费者,再看`work01`这个工作线程:


![在这里插入图片描述](https://img-blog.csdnimg.cn/0ae64c0f67ac469aba5110e2bde6c5c1.png#pic_center)  
 再看`work02`这个工作线程:


![在这里插入图片描述](https://img-blog.csdnimg.cn/a3d464f806354245964d92829432ac67.png#pic_center)


很明显,当有多个消息的时候,工作线程是通过轮询的方式去消费消息的。


## 四. 消息应答机制


### 4.1 消息应答概念


消息应答机制(`Message Acknowledgment`)是一种确认消息是否被消费者成功处理的机制。在消息队列中,当一个消息被消费者获取并处理后,需要向消息队列发送一个确认信息,告诉消息队列该消息已经被消费者成功处理了。


在消息队列中,如果某个消息没有被消费者成功处理,那么它将**一直留在消息队列中,直到被正确处理为止**。如果没有消息应答机制,则消息队列无法知道哪些消息是否被成功处理。


通常情况下,消息应答机制分为两种模式:**自动应答模式和手动应答模式。**


**自动应答模式**


自动应答是指当消费者从队列中接收到消息时,立即将消息从队列中删除,而不需要等待消费者明确地向RabbitMQ确认是否已经处理完成。


自动应答的优点是消费者能够迅速地将消息从队列中移除,提高了消费者的消息处理效率和吞吐量。另外,它使得消息处理变得简单,因为消费者不需要处理应答确认的逻辑。


自动应答也存在一些缺点。如果消费者在处理消息时发生了异常,这些消息将会被丢失而无法重新投递。


如果消费者处理消息的时间很长,而没有明确的确认机制,消息队列无法知道消息是否已被处理,从而导致消息被多次处理,甚至可能导致消息丢失。


**手动应答模式**


在实际生产环境中,一般采用手动应答的方式来保证消息的可靠处理。


手动应答是指在消费者处理完一条消息后,需向 `RabbitMQ` 显示地发送一个确认应答信号。


这种方式需要调用`channel.basicAck()`方法来通知当前消息已经被消费,可以将其从队列中删除。


如果在消息处理过程中发生了异常,可以调用`channel.basicNack()`方法来**拒绝当前消息并将其重新放回队列中**。此外,还可以使用`channel.basicReject()`方法**将消息拒绝并将其丢弃**。


上面这三种方法需要记住后面详细说明。


手动应答的优点是能够保证消息的可靠处理,可以避免由于消费者处理失败而导致消息丢失的问题。


同时,手动应答可以根据实际情况自行控制消息的处理方式。


对于手动应答还有一个好处就是可以使用批量应答,在批量应答中,消费者可以一次性确认多个消息的处理结果,以提高消息确认的效率。


消费者可以使用`basicAck`方法的`multiple`参数来进行批量应答,例如:



channel.basicAck(deliveryTag, true)


其中`deliveryTag`表示消息的唯一标识,第二个参数决定是否批量确认多条消息。`true`表示批量处理消息。


这样,消费者就可以一次性确认多个消息的处理结果了。


对于第二个参数为`true`与`false`的区别:


例如,当调用`channel.basicAck(10, true)` 时,会确认 `Delivery Tag` 从 1 到 10 的所有消息。


而当调用 `channel.basicAck(10, false)` 时,只会确认`Delivery Tag`为 10 的这条消息。


![在这里插入图片描述](https://img-blog.csdnimg.cn/bbefe88618b845ee99d60a74eddf5fbf.png#pic_center)


手动应答的缺点是增加了代码的复杂度和实现的难度,需要开发人员自己处理消息的确认和拒绝操作。


手动应答也可能会导致消息处理的延迟,因为需要等待消费者确认消息后才能将其从队列中删除。


### 4.2 消息手动应答


#### 4.2.1 消息重新入队


如果消息出现上面所说的没有被正确处理掉,需要将消息重新放入消息队列中让其他消费者来消费,从而保证消息的准确性,如下图所示:


![在这里插入图片描述](https://img-blog.csdnimg.cn/e3a1a809db184bbda7d8b9a0af59555d.png#pic_center)


#### 4.2.2 消息手动应答代码实现


现在编写代码,用一个生产者和两个消费者来实现消息手动应答不丢失,然后重新入队被消费。


**生产者代码**



/**
* @author jektong
* @date 2023年05月13日 20:15
*/
public class Task2 {

// 队列名称
private static final  String QUEUE_NAME = "ack\_queue";

public static void main(String[] args) throws Exception{
    Channel channel = RabbitMqUtils.getChannel();
    // 声明队列
    channel.queueDeclare(QUEUE_NAME,false,false,false,null);
    // 发消息
    Scanner sc = new Scanner(System.in);
    while (sc.hasNext()){
        String msg = sc.next();
        channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
        System.out.println("生产者发送消息:" + msg);
    }
}

}


**消费者Work03代码**



/**
* @author jektong
* @date 2023年05月13日 20:23
*/
public class Work03 {

public static final String QUEUE_NAME = "ack\_queue";

public static void main(String[] args) throws IOException, TimeoutException {
    // 接收消息
    Channel channel = RabbitMqUtils.getChannel();
    System.out.println("消费者1处理消息时间较短");
    // 接收消息后处理
    DeliverCallback deliverCallback = (consumerTag, message)->{
        try {
            // 等待1s处理消息
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(new String(message.getBody()));
        // 手动应答
        /\*\*

* arg1:表示消息标识
* arg2:是否批量应答(之前详细说了此方法)
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
// 取消消息时的回调
CancelCallback cancelCallback = consumerTag->{
System.out.println(“消息消费被中断”);
};
// 手动应答为fasle
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}


对于消费者`Work04`代码只需修改一下等待时间即可。



// 等待30s后处理消息
TimeUnit.SECONDS.sleep(30);


让`Work04`在30秒中给它断开连接,达到让给它进行消费的消息会重新入队给消费者`Work03`进行消费(请自行测试)。


## 五. RabbiMQ消息持久化


### 5.1 消息持久化概念


上面只是处理了消息不被丢失的情况,但如果要保障当`RabbiMQ`服务停掉之后的消息不丢失,因为在默认的情况下,`RabbiMQ`会忽略队列与消息。


如果将消息标记为持久化,那么当RabbitMQ关闭或重新启动时,该消息将仍然存在,消息的持久性标志需要**同时设置队列和消息的标志**。


### 5.2 如何持久化


#### 5.2.1 队列持久化


上面创建的生产者并没有进行持久化,需要将要其进行持久化,需要标记为`durable=true`



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


需要注意,若之前队列未进行持久化需要将之前的队列进行删除,否则会出现错误。


打开消息管理界面证明队列已经被持久化:


![在这里插入图片描述](https://img-blog.csdnimg.cn/29e20e893cc5463e9eedc38fb00245a9.png#pic_center)


#### 5.2.2 消息持久化


要使发布的消息持久化,需要在消息属性中设置`MessageProperties.PERSISTENT_TEXT_PLAIN`属性,修改上述生产者的代码:



// 将消息保存到磁盘上
channel.basicPublish(“”,QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());


注意,将消息标记为持久化并不能保证它们会永久保存,因为RabbitMQ仍然可以丢失刚要写入磁盘,但是还未完全写入磁盘的消息。因此,要确保消息不会丢失,还需要**使用备份和复制策略(后面会说)**。


#### 5.2.3 不公平分发


在某些情况下,某个消费者的处理速度比其他消费者慢,这时就需要采用**不公平分发**的方式,即使某些消费者处于忙碌状态,也将消息发送给它们。


在不公平分发中,`RabbitMQ`仍然会将每个消息发送给所有的消费者,但是会将消息发送给第一个处于空闲状态的消费者。


因此,快速处理消息的消费者将会更快地获得更多的消息,而处理较慢的消费者将会逐渐减少接收到的消息数量。


不公平分发的实现方法与公平分发相同,只需不使用`basicQos`方法设置`prefetchCount`即可,将`Work03`与`Work04`加入以下代码:



int prefetchCount = 1;
// 使用不公平分发
channel.basicQos(prefetchCount);


![在这里插入图片描述](https://img-blog.csdnimg.cn/5197de00243543a78a7843af304cb0e9.png#pic_center)


#### 5.2.4 预取值


当消费者连接到队列并开始接收消息时,RabbitMQ会按照预取值设置来决定一次性发送给消费者的消息数量。


预取值的设置是在消费者端生效的,而不是在队列端。每个消费者可以独立设置自己的预取值。


因此不同的消费者可以根据自身的处理能力和需求来设置合适的预取值。


比如一开始有7条消息,通过设置预取值给消费者1与2分别发送2条与5条。使用通过`channel.basicQos(prefetchCount)`设置预取值。


![在这里插入图片描述](https://img-blog.csdnimg.cn/edb4122914cc4775b330024cdedb7106.png#pic_center)  
 将`Work03`修改以下代码:



// 设置预取值
int prefetchCount = 2;
channel.basicQos(prefetchCount);


将`Work04`修改以下代码:



// 设置预取值
int prefetchCount = 5;
channel.basicQos(prefetchCount);


`channel.basicQos(prefetchCount)`此方法参数值若为0则是轮询分发,1是不公平分发,其它值都是设置预取值。


## 六. 发布确认


### 6.1 发布确认概述


发布确认的原理是基于AMQP协议中的信道(Channel)级别的确认机制。


当生产者发送一条消息到RabbitMQ时,会在信道上启用发布确认模式。一旦启用了发布确认模式,每次发送消息时,生产者都会为该消息分配一个唯一的传递标签(Delivery Tag)。


RabbitMQ在接收到消息后,会发送一个确认消息(ACK)给生产者,通知生产者消息已成功接收。确认消息中包含了相应消息的传递标签。


生产者可以通过三种方式进行发布确认的处理:**单个确认发布,批量确认发布与异步确认发布。**


### 6.2 单个确认发布


一种简单的确认模式,使用**同步确认发布**的方式,单个消息确认的基本流程如下:


1. 生产者发送消息到`RabbitMQ`。
2. 生产者等待`RabbitMQ`的确认消息。
3. 如果在指定的超时时间内收到了确认消息,表示消息已成功接收,生产者可以继续发送下一条消息。
4. 如果超时时间内未收到确认消息,生产者可以根据需求进行相应的处理,例如重发消息、记录日志、执行补偿逻辑等。


缺点就是发布速度很慢,下面用代码实现此种方式并查看这种方式发送消息的时间。



public static void publishMessageSingle() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 队列声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,true,false,false,null);
// 开启发布确认
channel.confirmSelect();
// 开始时间
long startTime = System.currentTimeMillis();
// 批量发送消息
for (int i = 0; i < 1000; i++) {
String msg = i + “”;
channel.basicPublish(“”,queueName,null,msg.getBytes());
// 单个消息发布确认
boolean flag = channel.waitForConfirms();
if(flag){
System.out.println(“消息发送成功”);
}
}
// 结束时间
long endTime = System.currentTimeMillis();
System.out.println(“发布”+1000+“个单独确认消息耗时”+(endTime-startTime)+“ms”);
}


运行代码,发现耗时·`410ms`:


![在这里插入图片描述](https://img-blog.csdnimg.cn/f5a33f0078bf439ba3ddfcd64e199295.png#pic_center)


### 6.3 批量确认发布


批量消息确认模式下,生产者可以一次性发送多条消息,并在所有消息都被成功接收后进行确认。


生产者会设置一个确认窗口(`Confirm Window`),窗口大小决定了可以未确认的消息数量。


当窗口中的所有消息都被确认后,生产者会收到一个批量确认消息(`Batch Ack`)。


批量消息确认的基本流程如下:


1. 生产者发送多条消息到`RabbitMQ`。
2. 生产者设置一个确认窗口大小。
3. 当发送的消息数量达到确认窗口大小时,生产者等待RabbitMQ的批量确认消息。
4. 如果收到批量确认消息,表示窗口中的所有消息都已成功接收,生产者可以继续发送下一批消息。
5. 如果超时时间内未收到批量确认消息,生产者可以根据需求进行相应的处理,例如重发消息、记录日志、执行补偿逻辑等。


通过批量确认消息,生产者可以确保一批消息的完整性,适用于对消息完整性要求不那么严格的场景。


但是如果出现了问题,就并不知道哪个消息是否出现了问题。


下面是批量发布消息确认的实现:



/**
* 批量消息确认
* @throws Exception
*/
public static void publishMessageBatch() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 队列声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,true,false,false,null);
// 开启发布确认
channel.confirmSelect();
// 开始时间
long startTime = System.currentTimeMillis();
// 批量确认消息的大小
int batchSize = 100;
// 批量发送消息
for (int i = 0; i < 1000; i++) {
String msg = i + “”;
channel.basicPublish(“”,queueName,null,msg.getBytes());

        if(i % batchSize == 0){
            // 消息发布确认
            channel.waitForConfirms();
            System.out.println("消息发送成功");
        }
    }
    // 结束时间
    long endTime = System.currentTimeMillis();
    System.out.println("发布"+1000+"个单独确认消息耗时"+(endTime-startTime)+"ms");
}

运行代码,发现耗时`34ms`:


![在这里插入图片描述](https://img-blog.csdnimg.cn/f31ccc9eb8274df09e81c55bb28e7b41.png#pic_center)


### 6.4 异步确认发布


异步确认的性价比比上面两种方式都要高,原因就是生产者发送消息后不会立即等待确认消息,而是继续发送下一条消息。


同时,生产者会通过一个异步回调(Callback)函数来处理确认消息的回调操作,来确认消息是否发送成功。


异步消息确认的基本流程如下:


1. 生产者发送消息到`RabbitMQ`。
2. 生产者不会立即等待确认消息,而是继续发送下一条消息。
3. 生产者注册一个异步回调函数,用于处理确认消息。
4. 当RabbitMQ接收到消息并完成处理后,会异步发送确认消息给生产者。
5. 一旦生产者收到确认消息,就会触发回调函数执行相应的逻辑,比如记录日志、更新状态等。


![在这里插入图片描述](https://img-blog.csdnimg.cn/72dce84d5e41447b89104035801150b5.png#pic_center)  
 下面是消息异步确认的实现:



/**
* 异步消息确认
* @throws Exception
*/
public static void publishMessageAsync() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 队列声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,true,false,false,null);
// 开启发布确认
channel.confirmSelect();
// 开始时间
long startTime = System.currentTimeMillis();
// 消息处理成功回调
ConfirmCallback ackCallback = (var1,var2)->{
System.out.println(“未确认的消息” + var1);
};
// 消息未处理成功回调
ConfirmCallback nackCallback = (var1,var2)->{
System.out.println(“消息发送成功了” + var1);
};
// 消息监听器
channel.addConfirmListener(ackCallback,nackCallback);
// 批量发送消息
for (int i = 0; i < 1000; i++) {
String msg = i + “”;
channel.basicPublish(“”,queueName,null,msg.getBytes());
}
// 结束时间
long endTime = System.currentTimeMillis();
System.out.println(“发布”+1000+“个异步确认消息耗时”+(endTime-startTime)+“ms”);
}


运行代码,发现耗时`18ms`:


![在这里插入图片描述](https://img-blog.csdnimg.cn/44e926d7baa74a06847b54ab71f94f4e.png#pic_center)


### 6.5 异步未确认消息处理


对于异步确认中未确认消息的处理,有一个方案就是将未确认的消息放到一个基于内存的能被发布的线程访问的队列中。


比如使用`ConcurrentLinkedQeque`在多个线程之间进行消息传递。多个线程可以同时发送消息与接收消息,实现消息的并发传递。


在发送的消息时,记录发送过的消息,在回调函数删除已经确认成功的消息,代码实现如下:



/\*\*

* 异步消息确认
* @throws Exception
*/
public static void publishMessageAsync() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 队列声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,true,false,false,null);
// 开启发布确认
channel.confirmSelect();
/**
* 安全的线程有序的哈希表,就是一个容器,适用于高并发
* 1.将序号与消息关联
* 2.轻松批量删除
* 3.支持高并发
*/
ConcurrentSkipListMap<Long,String> concurrentSkipListMap = new ConcurrentSkipListMap<>();
// 开始时间
long startTime = System.currentTimeMillis();
// 消息处理成功回调
// var1: 消息序列号
// var2: 是否批量
ConfirmCallback ackCallback = (var1,var2)->{
if(var2){
// 删除已经确认的消息,剩下的就是未确认的消息
ConcurrentNavigableMap<Long, String> confirmed =
concurrentSkipListMap.headMap(var1);
confirmed.clear();
}else{
concurrentSkipListMap.remove(var1);
}
System.out.println(“确认的消息” + var1);
};
// 消息未处理成功回调
ConfirmCallback nackCallback = (var1,var2)->{
String unConfirm = concurrentSkipListMap.get(var1);
System.out.println(“未确认的消息是:”+unConfirm+“,消息发送失败了失败标记:” + var1);
};
// 消息监听器
channel.addConfirmListener(ackCallback,nackCallback);
// 批量发送消息
for (int i = 0; i < 1000; i++) {
String msg = i + “”;
channel.basicPublish(“”,queueName,null,msg.getBytes());
concurrentSkipListMap.put(channel.getNextPublishSeqNo(),msg);
}
// 结束时间
long endTime = System.currentTimeMillis();
System.out.println(“发布”+1000+“个异步确认消息耗时”+(endTime-startTime)+“ms”);
}


`ConcurrentSkipListMap`是一个线程安全的有序哈希表,适用于高并发环境。它可以将消息的序列号与消息内容关联起来,并支持高并发的读写操作。


用它来实现通过回调函数处理消息的确认和未确认情况。


## 七. 交换机


### 7.1 交换机是什么


交换机(`Exchange`)是消息的路由中心,负责**接收生产者发送的消息**,并根据一定的路由规则将消息**路由到一个或多个队列中**,决定消息从生产者到达队列的路径。


在`RabbitMQ`中有这几个常见的路由规则:**直接模式,主题模式,头部模式**和**Fanout模式**等之后细说。在`RabbitMQ`提供的管理界面可以看到:


![在这里插入图片描述](https://img-blog.csdnimg.cn/27de11e8387b4c8a9e939363f75d3b64.png#pic_center)


交换机主要有以下几个作用:


1. **接收消息**:交换机接收来自生产者的消息,并负责将消息发送到合适的队列。
2. **路由消息**:交换机根据预定义的路由规则将消息路由到一个或多个队列。
3. **分发消息**:如果一个交换机路由消息到多个队列,那么交换机会将消息复制到所有符合路由规则的队列中,实现消息的广播或者多播。
4. **支持不同的路由模式**:交换机可以根据不同的路由模式来决定如何路由消息,例如直接交换、扇形交换、主题交换等


对于交换机还有和它相关的一些概念例如绑定(`bindings`),很好理解,就是交换机与队列之间可以通过一个`RoutingKey`将两者绑定,这样可以将想要的消息发送至指定的队列中。


#pic\_center


### 7.2 fanout交换机


在`fanout`模式下,交换机会将消息广播到所有与之绑定的队列,无论消息的路由键是什么。


`fanout`模式的特点如下:


1. **广播消息**:Fanout交换机会将消息复制到所有与之绑定的队列中,实现消息的广播。每个消费者都会收到相同的消息副本。
2. **忽略路由键**:Fanout交换机忽略消息的路由键,它只关注与之绑定的队列。
3. **适用于发布/订阅模式**:Fanout模式常用于**发布/订阅模式**,其中一个生产者发送消息,多个消费者接收并处理消息。


![在这里插入图片描述](https://img-blog.csdnimg.cn/1decb72ba9344a8ca6ee4e86473f56a1.png#pic_center)


下面用代码来测试一下`fanout`模式:


**消费者代码**:



package com.javadouluo.abbitmq.five;

import com.javadouluo.abbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
* @author jektong
* @date 2023年05月24日 22:23
*/
public class ReceiveLogs01 {

public static void main(String[] args) throws IOException, TimeoutException {
    Channel channel = RabbitMqUtils.getChannel();
    // 声明一个交换机logs,类型是fanout
    channel.exchangeDeclare("logs","fanout");
    // 声明一个临时队列,名称是随机的
    // 当消费者断开与队列的连接时,队列自动删除
    String queueName = channel.queueDeclare().getQueue();
    // 绑定交换机与对列
    channel.queueBind(queueName,"logs","");
    System.out.println("将消息打印到控制台上......");
    // 接收消息后处理
    DeliverCallback deliverCallback = (consumerTag, message)->{
        System.out.println("01接收的消息是:"+ new String(message.getBody()));
    };
    // 取消消息时的回调
    CancelCallback cancelCallback = consumerTag->{
        System.out.println("消息消费被中断");
    };
    channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}

}


**生产者代码**:



package com.javadouluo.abbitmq.five;

import com.javadouluo.abbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

/**
* @author jektong
* @date 2023年05月27日 10:12
*/
public class EmitLog {

public static void main(String[] args) throws IOException, TimeoutException {
    Channel channel = RabbitMqUtils.getChannel();
    // 声明一个交换机logs,类型是fanout
    channel.exchangeDeclare("logs","fanout");
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNext()){
        String msg = scanner.next();
        // 发送消息
        channel.basicPublish("logs","",null,msg.getBytes());
        System.out.println("生产者发出消息:" + msg);
    }
}

}


将上面的消费者复制两份,然后启动生产者与消费者,通过生产者发送消息,发现两个消费者都收到了消息,这就是`fanout`模式下的广播消息的特点:


![在这里插入图片描述](https://img-blog.csdnimg.cn/cf0516e9395445a3aa19b3b59aef46cc.png#pic_center)


同时在管理平台上也可以看到创建的交换机:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/62eec377f40243f4ab0396b341690078.png#pic_center)


### 7.3 direct交换机


直接交换机(direct)主要特点就是绑定的路由键是不一样的,它还有一个功能就是实现**多重绑定**。


**多重绑定**就是直接交换机可以有多个路由键来绑定一个交换机,如下图所示:


![在这里插入图片描述](https://img-blog.csdnimg.cn/573e6ca12e4e4077b55a015bab15b591.png#pic_center)


下面用代码实现上述功能:


**消费者DirectReceiveLogs01代码**:



package com.javadouluo.abbitmq.six;

import com.javadouluo.abbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
* @author jektong
* @date 2023年06月28日 0:58
*/
public class DirectReceiveLogs01 {

// 队列名称
public static  final String EXCHANGE_NAME = "direct\_logs";

public static void main(String[] args) throws IOException, TimeoutException {
    Channel channel = RabbitMqUtils.getChannel();
    // 声明一个交换机logs,类型是direct
    channel.exchangeDeclare(EXCHANGE_NAME,"direct");
    // 声明一个队列
    channel.queueDeclare("console",false,false,false,null);
    // 绑定交换机与对列
    channel.queueBind("console",EXCHANGE_NAME,"info");
    channel.queueBind("console",EXCHANGE_NAME,"warn");
    // 接收消息后处理
    DeliverCallback deliverCallback = (consumerTag, message)->{
        System.out.println("DirectReceiveLogs01接收的消息是:"+ new String(message.getBody()));
    };
    // 取消消息时的回调
    CancelCallback cancelCallback = consumerTag->{
        System.out.println("消息消费被中断");
    };
    channel.basicConsume("console",true,deliverCallback,cancelCallback);
}

}


**消费者DirectReceiveLogs02代码**:



package com.javadouluo.abbitmq.six;

import com.javadouluo.abbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
* @author jektong
* @date 2023年06月28日 0:58
*/
public class DirectReceiveLogs02 {

// 队列名称
public static  final String EXCHANGE_NAME = "direct\_logs";

public static void main(String[] args) throws IOException, TimeoutException {
    Channel channel = RabbitMqUtils.getChannel();
    // 声明一个交换机logs,类型是direct
    channel.exchangeDeclare(EXCHANGE_NAME,"direct");
    // 声明一个队列
    channel.queueDeclare("disk",false,false,false,null);
    // 绑定交换机与对列
    channel.queueBind("disk",EXCHANGE_NAME,"error");
    // 接收消息后处理
    DeliverCallback deliverCallback = (consumerTag, message)->{
        System.out.println("DirectReceiveLogs02接收的消息是:"+ new String(message.getBody()));
    };
    // 取消消息时的回调
    CancelCallback cancelCallback = consumerTag->{
        System.out.println("消息消费被中断");
    };
    channel.basicConsume("disk",true,deliverCallback,cancelCallback);
}

}

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

final String EXCHANGE_NAME = “direct_logs”;

public static void main(String[] args) throws IOException, TimeoutException {
    Channel channel = RabbitMqUtils.getChannel();
    // 声明一个交换机logs,类型是direct
    channel.exchangeDeclare(EXCHANGE_NAME,"direct");
    // 声明一个队列
    channel.queueDeclare("disk",false,false,false,null);
    // 绑定交换机与对列
    channel.queueBind("disk",EXCHANGE_NAME,"error");
    // 接收消息后处理
    DeliverCallback deliverCallback = (consumerTag, message)->{
        System.out.println("DirectReceiveLogs02接收的消息是:"+ new String(message.getBody()));
    };
    // 取消消息时的回调
    CancelCallback cancelCallback = consumerTag->{
        System.out.println("消息消费被中断");
    };
    channel.basicConsume("disk",true,deliverCallback,cancelCallback);
}

}

[外链图片转存中…(img-qJsgXdt9-1715685666311)]
[外链图片转存中…(img-V2kqTIBW-1715685666312)]
[外链图片转存中…(img-dT4Nniw1-1715685666312)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值