一、交换机
1、类型
RabbitMQ共有四种交换机,交换机主要决定发布的消息该以什么的方式进入队列(queue)
四种类型如下:
Name(交换机类型) | Default pre-declared names(预声明的默认名称) |
---|---|
Direct exchange(直连交换机) | (Empty string) and amq.direct |
Fanout exchange(扇型交换机) | amq.fanout |
Topic exchange(主题交换机) | amq.topic |
Headers exchange(头交换机) | amq.match (and amq.headers in RabbitMQ) |
交换机本身还可以携带属性,可以设置状态:持久(durable)、暂存(transient)。这里暂不介绍,如有兴趣可以翻阅文档:http://rabbitmq.mr-ping.com/AMQP/AMQP_0-9-1_Model_Explained.html
- 默认交换机
当我们使用RabbitMQ时,如果不指定交换机的类型,那么Rabbit会使用默认的一个交换机,这个默认的交换机类型是一个直连交换机(direct),后续新建的队列(queue)都会自动绑定到这个默认交换机上,绑定的路由键就是队列的名称,注意这个默认交换机的名称是一个空字符串 " "
// 可通过这个代码,在指定的虚拟主机vm上创建一个特定类型的交换机
channel.exchangeDeclare("exchange1","fanout");
// 后续发布消息可通过basicPublish方法的exchange参数指定交换机名称
// 如果第一个参数设置为空字符串"" 那么就是使用默认的直连交换机
channel.basicPublish("exchange1", QUEUE_NAME, null, msg.getBytes());
-
直连交换机(direct)
直连交换机往往用于做单播路由,绑定到直连交换机的队列名称和路由名称相同,那么直连交换机在路由消息的时候,会直接将队列名称作为路由键,把消息路由到该队列中 -
扇型交换机(fanout)
扇形交换机通常用来做广播,它会把消息路由给绑定在该交换机的所有队列,与绑定的路由键无关 -
主题交换机(topic)
主题交换机类似扇形的升级,扇形交换机是无差别发送,主题交换机会聪明一些,可以筛选路由键,将消息发给一个或者多个队列。
比如我们有三个队列,与topic绑定的路由键分别是(user.#)(money.#)(address.#)
那么发布消息的路由键为user.age、user.name的话,这两个消息就会被路由到user.#所对应的队列中,money.count的消息会被路由到money.#所对应的队列中,以此类推
其中的符号“#”可以匹配一个或者多个词,符号"*"可以配一个词 -
头部交换机
个人感觉可以视为直连交换机的另一种形式,直连交换之使用路由键点对点进行发布,头部交换机不使用消息发布时的路由键了,而是使用消息头里面的信息与路由进行匹配
二、消费类型
- 交换机的不同类型,决定消息以什么样的方式从交换机分发到队列中。
- 消费类型决定消息从队列中如何到消费者
RabbitMQ共提供了六种消费
1、简单模式
简单模式下是一个消费者在这里消费,队列中的所有消息都给这一个消费者,没有竞争
2、工作队列
与简单模式不同的是,工作队列是存在多个消费者消费同一个队列,并且同一条消息只能被一个消费者消费,此时RabbitMQ会有两种消息发送策略
- 轮询发送(默认):每个消费者发送一条,是不同的一条,比如有两个消费者、三个消息,消费者1拿到消息1、消费者2拿到消息2,因为是轮询,所以接下来的消息3会发送给消费者1
- 公平发送:根据消费者的消费能力进行公平发送
3、发布/订阅
发布订阅模式使用的是扇形交换机,其原理是基于扇形交换机的,队列绑定到交换机时可以不指定路由名称,使用空字符串""代替即可,代码如下:
/**
* 生产者代码
**/
public class ProducerFanout {
// 队列名称
public static final String QUEUE_NAME = "q_test_01";
public static final String QUEUE_NAME2 = "q_test_02";
// 交换机名称
public static final String FANOUT_EXCHANGE = "fanout_queue_1";
public static void main(String[] args) throws Exception {
// 获取连接
Connection connection = getConnection();
Channel channel = connection.createChannel();
String msg = "测试消息";
// 创建一个扇形交换机
channel.exchangeDeclare(FANOUT_EXCHANGE, "fanout");
// 创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueDeclare(QUEUE_NAME2, false, false, false, null);
// 将两个队列绑定到同一个扇形交换机上,第三方参数为路由名称,设为空字符串
channel.queueBind(QUEUE_NAME, FANOUT_EXCHANGE, "");
channel.queueBind(QUEUE_NAME2, FANOUT_EXCHANGE, "");
//发布消息到交换机
for (int i = 0; i < 10; i++) {
channel.basicPublish(FANOUT_EXCHANGE, "", null, (msg + i).getBytes());
}
//关闭通道和连接
channel.close();
connection.close();
}
/**
* 获取连接
**/
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("testhost");
factory.setUsername("admin");
factory.setPassword("1111");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
/**
* 消费者代码
* 消费者2的代码和这里一模一样,只需要把订阅的队列名称改为2就可以了
**/
public class ConsumerClientFanout {
public static void main(String[] args) throws Exception {
Connection connection = Producer.getConnection();
Channel channel = connection.createChannel();
DeliverCallback deliverCallback = (consumerTag, message) -> {
String message1 = new String(message.getBody());
System.out.println("DeliverCallback:" + consumerTag + "-" + message1);
};
CancelCallback cancelCallback = consumerTag -> {
System.out.println("CancelCallback:" + consumerTag);
};
// 订阅队列1
channel.basicConsume(ProducerFanout.QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
消费者1收到的消息
消费者2收到的消息
经过测试这个路由key不论设为什么,只要绑定到扇形交换的队列都会收到消息,而且不会通过路由key筛选队列
特别说明:
很多代码会在消费者和生产者的代码中都写上队列、交换机的创建代码等,个人理解是因为不一定是消费者先执行还是生产者先执行,比如消费者没有写创建那部分代码,那么消费者先执行的话,会报错误(订阅的队列不存在等)所以可以在两边都写上创建的代码,对于RabbitMQ来说相同的代码,多次执行的话,只有第一次执行才会生效,后面几次会检测到要创建的资源(交换机、队列)已经存在了,所以什么都不做,也不会报错
4、路由模式
路由模式使用的直连交换机,因为要使用路由键,原理基本就与直连交换机的原理一样,只不过用在了消息发送上
public class ProducerDirect {
public static String QUEUE_NAME = "q_test_01";
public static String QUEUE_NAME2 = "q_test_02";
public static final String FANOUT_EXCHANGE = "fanout_queue_1";
public static void main(String[] args) throws Exception {
Connection connection = getConnection();
Channel channel = connection.createChannel();
String msg = "测试消息";
String msg2 = "测试消息二";
// 创建一个直连交换机
channel.exchangeDeclare(FANOUT_EXCHANGE, "direct");
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueDeclare(QUEUE_NAME2, false, false, false, null);
// 这里绑定的时候指定路由key
channel.queueBind(QUEUE_NAME, FANOUT_EXCHANGE, "ad1");
channel.queueBind(QUEUE_NAME2, FANOUT_EXCHANGE, "ad2");
// 发布消息时指定路由key
for (int i = 0; i < 10; i++) {
channel.basicPublish(FANOUT_EXCHANGE, "ad1", null, (msg + i).getBytes());
channel.basicPublish(FANOUT_EXCHANGE, "ad2", null, (msg2 + i).getBytes());
}
//关闭通道和连接
channel.close();
connection.close();
}
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("localhost");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("testhost");
factory.setUsername("admin");
factory.setPassword("1111");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
5、主题模式
主题模式使用主题交换机,消息发送机制也就是主题交换机的机制,代码方面基本和路由模式一样,只不过在路由key上使用通配符就可以了
三、特别注意:
交换机类型和消费类型有类似的概念,因为消费类型是基于交换机类型来实现的,主要区别:
1、交换机决定消息从路由到队列的方式
2、消费类型描述的是消息从队列发送到消费者的方式