交换机类型及其详解:
5. 交换机
在上一节中,我们创建了一个工作队列。假设工作队列背后,每个人物都恰好交付给一个消费者(工作进程)。在这一部分中,会将消息传达给多个消费者,这种模式被称为 “发布/订阅”。
5.1 Exchanges
5.1.1 Exchanges 概念
- RabbitMQ 消息传递模型的核心思想是:生产者生产的消息从来不会直接发送到队列。实际上,通常生产者都不知道这些消息发送到了哪些队列中。
- 相反,生产者只能将消息发送到交换机(exchange)。交换机的工作内容非常简单,一方面它接受来自生产者的消息,另一方面将这些消息推入队列。
- 交换机必须知道如何处理收到的消息。是应该把这些消息放入特定队列还是说放到许多队列,或者直接丢弃,这就由交换机的类型决定。
5.1.2 Exchanges 的类型
-
总共有以下类型:
直接(direct)->路由类型,主题(topic),标题(headers)->头类型, 扇出(fanout)->发布/订阅类型
5.1.3 无名 Exchange
在之前我们对 Exchange 一无所知,但仍然能够将消息发送到队列。之前能够实现的原因是使用的是默认交换,通过空字符串(“ ”)进行标识。
第一个参数是交换机的名称。空字符串表示默认或者无名交换机:消息能路由发送到队列中其实是由 routingKey(bindingKey)实现的,如果它存在的话。
5.2 临时队列
- 队列的名称对我们来说至关重要,需要指定消费者去消费哪个队列的消息。
每当连接 RabbitMQ 时,都需要一个全新的空队列,为此可以创建一个具有随机名称的队列,或者让服务器随机选一个队列名称。其次一旦我们断开了消费者的连接,队列将被自动删除。
-
创建临时队列的方式如下:
String queue = channel.queueDeclare().getQueue();
-
创建出来的队列是这样的:
5.3 绑定(binding)
绑定是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和哪个队列进行了绑定关系。
5.4 Fanout 扇出模式(发布/订阅)
5.4.1 Fanout 介绍
Fanout 会将接收到的所有消息广播到它知道的所有队列中。
5.4.2 Fanout 实战
logs(交换机名称)和临时队列的绑定关系如下:
编写两个消费者,一个生产者,采用扇出模式,查看具体情况
消费者 ReceiveLog01:
package com.example.five;
import com.example.utils.RabbitUtils;
import com.example.utils.SleepUtils;
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 且听风吟
* @version 1.0
* @description: 消息接收
* @date 2022/4/19 0019 11:02
*/
public class ReceiveLog01 {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//声名一个交换机 参数 1:交换机名称 2:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
/**
* 声名一个临时队列 队列的名称是随机的
* 当消费者断开与队列的连接的时候,队列就会自动被删除
*/
String queue = channel.queueDeclare().getQueue();
//绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey 此处为空字符串
channel.queueBind(queue,EXCHANGE_NAME,"");
System.out.println("ReceiveLog01等待接收消息,把接收到的消息打印在屏幕上···");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLog01接收到的消息:"+new String(message.getBody(),"UTF-8"));
};
//消费者取消消息的回调
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息消费被中断");
};
/**
* 消费者消费(接收)消息,参数含义如下:
* 1.消费哪个队列
* 2.消费之后是否要自动应答
* 3.消费者成功消费消息的回调
* 4.消费者取消消费消息的回调
*/
channel.basicConsume(queue,true,deliverCallback,cancelCallback);
}
}
消费者 ReceiveLog02:
package com.example.five;
import com.example.utils.RabbitUtils;
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 且听风吟
* @version 1.0
* @description: 消息接收
* @date 2022/4/19 0019 11:02
*/
public class ReceiveLog02 {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//声名一个交换机 参数 1:交换机名称 2:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
/**
* 声名一个临时队列 队列的名称是随机的
* 当消费者断开与队列的连接的时候,队列就会自动被删除
*/
String queue = channel.queueDeclare().getQueue();
//绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey 此处为空字符串
channel.queueBind(queue,EXCHANGE_NAME,"");
System.out.println("ReceiveLog02等待接收消息,把接收到的消息打印在屏幕上···");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLog02接收到的消息:"+new String(message.getBody(),"UTF-8"));
};
//消费者取消消息的回调
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息消费被中断");
};
/**
* 消费者消费(接收)消息,参数含义如下:
* 1.消费哪个队列
* 2.消费之后是否要自动应答
* 3.消费者成功消费消息的回调
* 4.消费者取消消费消息的回调
*/
channel.basicConsume(queue,true,deliverCallback,cancelCallback);
}
}
生产者 EmitLog 发消息给交换机:
package com.example.five;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 发消息给交换机
* @date 2022/4/19 0019 12:15
*/
public class EmitLog {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//声名交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
/**
* 发送一个消息,参数含义如下:
* 1.发送到哪个交换机,null表示使用默认交换机
* 2.路由的key值是哪个 本次key=""
* 3.其他参数信息
* 4.发送消息的消息体(发送消息的二进制码)
*/
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
}
}
}
-
生产者向交换机发送 11、22、33、44 这几条消息
-
观察两个消费者接收消息的情况如下,它们都接收到了来自生产者的消息:
综上所述,在 Fanout 模式下,交换机 logs 将消息发送给了它知道的所有队列(两个临时队列queue)。
可以得知,Fanout 模式会将接收到的所有消息广播到它知道的所有队列中。
5.5 Direct exchange 直接交换机(路由模式)
5.5.1 回顾
- 在上一节中,构建了一个简单的日志记录系统。能够向众多的接收者广播消息。
- 在本节我们将添加一些特别的功能,比如只向某个消费者发布部分消息,例如只把严重错误消息定向存储到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
绑定是交换机和队列之间的桥梁关系,也可以这么理解:队列只对它绑定的交换机的消息感兴趣。绑定用参数 routingKey 来表示,也可以称该参数为 bindingKey,绑定之后的意义由其交换类型决定。
5.5.2 Direct exchange 介绍
Fanout 这种交换机类型并不能给我们带来很大的灵活性——它只能进行无意识的广播,在这里我们将使用 direct 这种类型来进行替换,这种类型的工作方式是,消息只到交换机绑定的 routingKey 队列中去。
5.5.3 多重绑定
当然如果exchange的绑定类型是 direct ,但是它绑定的多个队列的 key 如果都相同,在这种情况下虽然绑定类型是 direct,但是它表现得就和 fanout 有点相似了,就跟广播差不多。
5.5.4 实战
-
创建两个队列 console 和 disk,console队列里信道有两个 RoutingKey,info 和 warning,disk 队列的信道中有一个 routingKey,error。
队列 console:
package com.example.six;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 消费者1
* @date 2022/4/21 0021 22:09
*/
public class ReceiveLogDirect01 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//声名一个交换机 参数 1:交换机名称 2:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//声名一个队列
channel.queueDeclare("console",false,false,false,null);
//绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey
channel.queueBind("console",EXCHANGE_NAME,"info");
channel.queueBind("console",EXCHANGE_NAME,"warning");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveDirect01接收到的消息:"+new String(message.getBody(),"UTF-8"));
};
/**
* 消费者消费(接收)消息,参数含义如下:
* 1.消费哪个队列
* 2.消费之后是否要自动应答
* 3.消费者成功消费消息的回调
* 4.消费者取消消费消息的回调
*/
channel.basicConsume("console",true,deliverCallback,consumerTag -> {});
}
}
队列 disk:
package com.example.six;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 消费者2
* @date 2022/4/21 0021 22:09
*/
public class ReceiveLogDirect02 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//声名一个交换机 参数 1:交换机名称 2:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//声名一个队列
channel.queueDeclare("disk",false,false,false,null);
//绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey
channel.queueBind("disk",EXCHANGE_NAME,"error");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveDirect02接收到的消息:"+new String(message.getBody(),"UTF-8"));
};
/**
* 消费者消费(接收)消息,参数含义如下:
* 1.消费哪个队列
* 2.消费之后是否要自动应答
* 3.消费者成功消费消息的回调
* 4.消费者取消消费消息的回调
*/
channel.basicConsume("disk",true,deliverCallback,consumerTag -> {});
}
}
生产者 Direct_logs:
package com.example.six;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 生产者
* @date 2022/4/21 0021 22:25
*/
public class DirectLogs {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//声名交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
/**
* 发送一个消息,参数含义如下:
* 1.发送到哪个交换机,null表示使用默认交换机
* 2.路由的key值是哪个
* 3.其他参数信息
* 4.发送消息的消息体(发送消息的二进制码)
*/
channel.basicPublish(EXCHANGE_NAME, "info", null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息:"+message);
}
}
}
-
生产者向 RoutingKey 为 info 的console队列发送消息:
-
观察两个队列接收到消息的情况:
只有console队列接收到了消息,说明 direct 交换机只会发送消息到与其绑定的队列中。
5.6 Topics (主题模式)
5.6.1 之前类型的问题
尽管使用 direct 交换机改进了我们的系统,但是仍然存在局限性—比如说我们想接收的日志类型有 info.base 和 info.advantage,某个队列只想 info.base 的消息,那这个时候 direct 就办不到了。这个时候就只能使用 topic 类型。
5.6.2 Topic 的要求
-
发送类型是 topic 交换机的 routingKey 不能随便写,必须满足一定要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说 “stock.nyse”, “queue.nyse”。当然,这个单词列表最多不能超过 255 个字节。
-
在这个规则列表中,其中有两个替换符是需要注意的:
*(星号)可以代表一个单词
#(井号)可以代表零个或者多个单词
5.6.3 topic 匹配案例
上图绑定关系如下:
-
Q1 绑定的是
中间带 orange 带三个单词的字符串
-
Q2 绑定的是
最后一个单词是 rabbit 的三个单词
第一个单词是 lazy 的多个单词
当队列绑定关系是下列这些情况时需要注意:
- 当一个队列绑定键是#,那么这个队列将接受所有数据,有点像fanout
- 如果队列绑定键中没有#和*出现,那么该队列的绑定类型就是direct了
5.6.4 实战
声名两个主题交换机及其队列。
两个消费者分别为 C1 和 C2,它们的队列分别为 Q1、Q2,连接队列 Q1 和交换机 topic 的 routingKey=* . orange . *,连接队列 Q2 和交换机 topic 的 routingKey = * . * . rabbit 和 lazy.#
消费者 C1:
package com.example.seven;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 声名主题交换机及其队列
* 消费者 C1
* @date 2022/4/23 0023 11:42
*/
public class ReceiveLogsTopic01 {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//声名一个交换机 参数 1:交换机名称 2:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声名一个队列
channel.queueDeclare("Q1", false, false, false, null);
//绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey
channel.queueBind("Q1", EXCHANGE_NAME, "*.orange.*");
System.out.println("等待接收消息。。。");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveTopic01接收到的消息:"+new String(message.getBody(),"UTF-8"));
System.out.println("接受队列:Q1"+" 绑定键:"+message.getEnvelope().getRoutingKey());
};
/**
* 消费者消费(接收)消息,参数含义如下:
* 1.消费哪个队列
* 2.消费之后是否要自动应答
* 3.消费者成功消费消息的回调
* 4.消费者取消消费消息的回调
*/
channel.basicConsume("Q1",true,deliverCallback,consumerTag -> {});
}
}
消费者C2:
package com.example.seven;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 声名主题交换机及其队列
* 消费者 C2
* @date 2022/4/23 0023 11:42
*/
public class ReceiveLogsTopic02 {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
//声名一个交换机 参数 1:交换机名称 2:交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声名一个队列
channel.queueDeclare("Q2", false, false, false, null);
//绑定交换机与队列 参数:1.队列名称 2.交换机名称 3.routingKey
channel.queueBind("Q2", EXCHANGE_NAME, "*.*.rabbit");
channel.queueBind("Q2", EXCHANGE_NAME, "lazy.#");
System.out.println("等待接收消息。。。");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveTopic02接收到的消息:"+new String(message.getBody(),"UTF-8"));
System.out.println("接受队列:Q2"+" 绑定键:"+message.getEnvelope().getRoutingKey());
};
/**
* 消费者消费(接收)消息,参数含义如下:
* 1.消费哪个队列
* 2.消费之后是否要自动应答
* 3.消费者成功消费消息的回调
* 4.消费者取消消费消息的回调
*/
channel.basicConsume("Q2",true,deliverCallback,consumerTag -> {});
}
}
生产者 EmitLogTopic:
package com.example.seven;
import com.example.utils.RabbitUtils;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
/**
* @author 且听风吟
* @version 1.0
* @description: 生产者
* @date 2022/4/23 0023 12:05
*/
public class EmitLogTopic {
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = RabbitUtils.getChannel();
/**
* Q1绑定的是:*.orange.*
* Q2绑定的是:*.*.rabbit lazy.3
*/
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit","被队列Q1、Q2接收到");
bindingKeyMap.put("lazy.orange.elephant","被队列Q1、Q2接收到");
bindingKeyMap.put("quick.orange.fox","被队列Q1接收到");
bindingKeyMap.put("lazy.fox","被队列Q2接收到");
bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定条件但只被Q2接受一次");
bindingKeyMap.put("quick.orange.male.rabbit","不匹配任何绑定会被丢弃");
bindingKeyMap.put("quick.brown.fox","不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit","匹配Q2,被Q2接收");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
String routingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes("UTF-8"));
System.out.println("生产者发出消息"+message);
}
}
}
-
启动生产者,轮流发送Map里存放的消息:
-
查看两个消费者接收到的消息: