在上一节中,我们创建了一个工作队列,使用默认交换机绑定。我们假设的是工作队列背后,每个任务都恰好交付给一个消费者(工作进程)。
在这一部分中,我们将做一些完全不同的事情-我们将消息传达给多个消费者。这种模式称为 ”发布/订阅”。
为了说明这种模式,我们将构建一个简单的日志系统。它将由两个程序组成,第一个程序将发出日志消息,第二个程序是消费者,其中我们会启动两个消费者,其中一个消费者接收到消息后把日志存储在磁盘,另外一个消费者接收到消息后把消息打印在屏幕上,事实上第一个程序发出的日志消息将广播给所有消费者者。
【简单模式】1个队列发送的消息只能被一个消费者消费
【发布订阅模式】多个队列发送的消息就可以被多个消费者消费
Exchanges 概念
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
生产者把消息发布 到 Exchange 上,消息最终到达队列 并被消费者接收,而 Binding 决定交 换器的消息应该发送到哪个队列。
Exchanges 的类型
直接(direct),也叫路由类型,只能交换机通过routingKey绑定的队列收到消息
主题(topic) :也是发布订阅,但是可以灵活的对所有绑定队列中的部分队列发送消息
标题(headers)
扇出(fanout),也叫发布订阅类型,广播模式,所有绑定队列都能收到消息
无名 exchange
前面部分我们对 exchange 一无所知,但仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换机,我们通过空字符串(“”)进行标识
//发送消息,持久化消息
channel.basicPublish("",queueName,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果它存在的话
之前我们使用的是具有特定名称的队列(还记得 hello 和 ack_queue 吗?)。队列的名称我们来说至关重要-我们需要指定我们的消费者去消费哪个队列的消息。
每当我们连接到 Rabbit 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
创建临时队列的方式如下:
String queueName = channel.queueDeclare().getQueue();
什么是 bingding 呢,binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队
列进行了绑定关系
交换机负责接收消息,根据 路由key123 进行路由,将消息发送到指定的 队列中,队列再把消息发送给消费者
Fanout 这种类型非常简单。它是将接收到的所有消息广播到它知道的所有队列中。类似qq群发消息,群里人都能收到消息,也好比村里的大喇叭,全村人都能听到,这就是广播。
将接收到的消息打印在控制台
package com.lian.exchange;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogs01 {
//自定义交换机名称
private final static String EXCHANGE_NAME = "log";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//信道声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
/**
* 生成一个临时的队列,队列的名称是随机的
* 当消费者断开和该队列的连接时,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 绑定交换机和队列
* 把该临时队列绑定我们的 exchange
* 其中 routingkey(也称之为 binding key)为空字符串
*/
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("等待接收消息,把接收到的消息打印在屏幕.....");
DeliverCallback deliverCallback = (consumerTag, delivery)->{
String message = new String(delivery.getBody(),"UTF-8");
System.out.println("控制台打印接收到的消息"+message);
};
CancelCallback cancelCallback = (consumerTag)->{};
/**
* 消费接收消息
* 1、队列名
* 2、默认是自动autoAck 是true,采用手动应答是false
* 3、成功回调
* 4、失败回调
*/
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
将接收到的消息存储在磁盘
package com.lian.exchange;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import org.apache.commons.io.FileUtils;
import java.io.File;
public class ReceiveLogs02 {
//自定义交换机名称
private final static String EXCHANGE_NAME = "log";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//信道声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/**
* 生成一个临时的队列,队列的名称是随机的
* 当消费者断开和该队列的连接时,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("等待接收消息,把接收到的消息写到文件.....");
DeliverCallback deliverCallback = (consumerTag, delivery)->{
String message = new String(delivery.getBody(),"UTF-8");
File file = new File("H:\\");
//使用apache的FileUtils工具类将字符串写入到file中
FileUtils.writeStringToFile(file,message,"UTF-8");
System.out.println("数据写入文件成功");
};
CancelCallback cancelCallback = (consumerTag)->{};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
发送消息给两个消费者接收
package com.lian.exchange;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import java.util.Scanner;
public class EmitLog {
//自定义交换机名称
private final static String EXCHANGE_NAME = "log";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
/**
* 1、信道声明一个exchange交换机
* 2、exchange的类型
*/
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//在控制台打印输出
Scanner scanner = new Scanner(System.in);
System.out.println("请输入信息");
while (scanner.hasNext()){
String message = scanner.next();
/**
* 发送消息
* 1、交换机名称
* 2、路由key routingKey为空串
* 3、是否持久化消息
* 4、消息
*/
channel.basicPublish(EXCHANGE_NAME,"", MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
System.out.println("生产者发出消息" + message);
}
}
}
在上一节中,我们构建了一个简单的日志记录系统。我们能够向许多接收者广播日志消息。在本节我们将向其中添加一些特别的功能-比方说我们只让某个消费者订阅发布的部分消息。例如我们只把严重错误消息定向存储到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
我们再次来回顾一下什么是 bindings,绑定是交换机和队列之间的桥梁关系。也可以这么理解:队列只对它绑定的交换机的消息感兴趣。
绑定用参数:routingKey 来表示也可称该参数为 binding key,创建绑定我们用代码
channel.queueBind(queueName, EXCHANGE_NAME, "routingKey");
绑定之后的意义由其交换类型决定
上一节中的我们的日志系统将所有消息广播给所有消费者,对此我们想做一些改变,例如我们希望将日志消息写入磁盘的程序仅接收严重错误(errros),而不存储哪些警告(warning)或信息(info)日志消息避免浪费磁盘空间。Fanout 这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播,在这里我们将使用== direct ==这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的routingKey 队列中去。
fanout扇出交换机的两个路由key相同,direct直接交换机的两个路由key不同
交换机和同一个队列绑定了两次,用了两个不同的路由key,也叫绑定key
package com.lian.direct;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogsDirect01 {
//声明交换机名称
private final static String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
//创建一个信道
Channel channel = RabbitMqUtils.getChannel();
//信道什么一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//声明队列名
String queueName = "console";
//信道声明队列,并持久化队列
channel.queueDeclare(queueName,true,false,false,null);
/**
* 绑定交换机和队列
* 把该队列绑定我们的 exchange
* 其中 routingkey(也称之为 binding key)为字符串 info
* 交换机可以绑定同一个队列多次,只要不同的 绑定key(routingKey)即可
*/
channel.queueBind(queueName,EXCHANGE_NAME,"info");
channel.queueBind(queueName,EXCHANGE_NAME,"warning");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery)->{
String message = new String(delivery.getBody(),"UTF-8");
System.out.println("控制台打印接收到的消息"+ message);
};
CancelCallback cancelCallback = (consumerTag)->{};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
package com.lian.direct;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogsDirect02 {
//声明交换机名称
private final static String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
//创建一个信道
Channel channel = RabbitMqUtils.getChannel();
//信道什么一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//声明队列名
String queueName = "disk";
//信道声明队列,并持久化队列
channel.queueDeclare(queueName,true,false,false,null);
/**
* 绑定交换机和队列
* 把该队列绑定我们的 exchange
* 其中 routingkey(也称之为 binding key)为字符串 error
* 交换机可以绑定同一个队列多次,只要不同的 绑定key(routingKey)即可
*/
channel.queueBind(queueName,EXCHANGE_NAME,"error");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery)->{
String message = new String(delivery.getBody(),"UTF-8");
System.out.println("控制台打印接收到的消息"+ message);
};
CancelCallback cancelCallback = (consumerTag)->{};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
package com.lian.direct;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import java.util.Scanner;
public class DirectLogs {
//声明交换机名称
private final static String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
//声明一个信道
Channel channel = RabbitMqUtils.getChannel();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
/**
* 发送消息
* 1、交换机名称
* 2、路由key routingKey
* 3、是否持久化消息
* 4、消息
*/
channel.basicPublish(EXCHANGE_NAME,"error", MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
System.out.println("生产者发出消息"+message);
}
}
}
直接交换机可是实现,生产者根据固定的路由key发送给指定与交换机绑定的队列
在上一个小节中,我们改进了日志记录系统。我们没有使用只能进行随意广播的 fanout 交换机,而是使用了 direct 交换机,从而有能实现有选择性地接收日志。
尽管使用 direct 交换机改进了我们的系统,但是它仍然存在局限性-比方说我们想接收的日志类型有info.base 和 info.advantage,某个队列只想 info.base 的消息,那这个时候 direct 就办不到了。这个时候就只能使用 topic 类型
缺点:direct直接交换机可以根据交换机的指定路由key,来发送给对应绑定的队列消息,但是只能发送给一个,而不能发送给多个队列,于是 topic交换机出来了
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开
这些单词可以是任意单词,比如说:“stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”.这种类型的。当然这个单词列表最多不能超过 255 个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词
可以给 Q1 和 Q2 都分别发送消息 ,功能类似 fanout扇出交换机
可以单独给 Q1或Q2发送消息,功能类似direct直接交换机
下图绑定关系如下
Q1–>绑定的是
中间带 orange 带 3 个单词的字符串(.orange.)
Q2–>绑定的是
最后一个单词是 rabbit 的 3 个单词(..rabbit)
第一个单词是 lazy 的多个单词(lazy.#)
上图是一个队列绑定关系图,下面是案例
quick.orange.rabbit 被队列 Q1Q2 接收到
lazy.orange.elephant 被队列 Q1Q2 接收到
quick.orange.fox 被队列 Q1 接收到
lazy.brown.fox 被队列 Q2 接收到
lazy.pink.rabbit 虽然满足两个绑定但只被队列 Q2 接收一次
quick.brown.fox 不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit 是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit 是四个单词但匹配 Q2
当队列绑定关系是下列这种情况时需要引起注意
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了
package com.lian.topic;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import java.util.HashMap;
import java.util.Map;
public class EmitLogTopic {
//声明交换机名
private final static String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
/**
* routingKey 用map保存
* Q1-->绑定的是
* 中间带 orange 带 3 个单词的字符串(*.orange.*)
* Q2-->绑定的是
* 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
* 第一个单词是 lazy 的多个单词(lazy.#)
*/
Map<String,String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到");
bindingKeyMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到");
bindingKeyMap.put("quick.orange.fox","被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到");
bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配 Q2");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
/**
* 发送消息
* 1、交换机名称
* 2、路由key routingKey为空串
* 3、是否持久化消息
* 4、消息
*/
channel.basicPublish(EXCHANGE_NAME,bindingKey, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
package com.lian.topic;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogsTopic01 {
//声明交换机名
private final static String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明 Q1 队列与绑定关系
String queueName="Q1";
//声明队列
channel.queueDeclare(queueName, false, false, false, null);
//将交换机和队列绑定
channel.queueBind(queueName,EXCHANGE_NAME,"*.orange.*");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery)->{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收队列 :"+queueName+" 绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
};
CancelCallback cancelCallback = (consumerTag)->{};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
package com.lian.topic;
import com.lian.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogsTopic02 {
//声明交换机名
private final static String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//声明 Q1 队列与绑定关系
String queueName="Q2";
//声明队列
channel.queueDeclare(queueName, false, false, false, null);
//将交换机和队列绑定
channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit");
channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery)->{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收队列 :"+queueName+" 绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
};
CancelCallback cancelCallback = (consumerTag)->{};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}