RabbitMQ是一个 异步通信中间件,可以在高并发下实现消峰限流,能实现消息的解耦,引入消息队列可以不用等待消息处理完成可以继续往下执行,不影响主要步骤的同步执行。在高并发下可以将任务放进队列中,让程序从队列中取出执行,而不是崩溃或者直接拒绝,起到消峰限流
重要组件
队列分消费者、生产者、以及队列,生产者将消息发送到队列中,消费者从队列中取出消息消费,生产者也就是客户端支持不同的工作而模式
- connection:connection底层长连接,:一个对象对应一个进程
- channel:可以频繁的销毁和创建,对应一次使用;
- queue:存放消息的容器,需要绑定交换机,每生成一个队列默认情况下都会绑定一个AMQP default的路由类型交换机“”也就是空字符,并且以队列的名称绑定路由;
服务器界面介绍
开启服务并打开连接http://127.0.0.1:15672/通过默认账户 guest/guest 登录
- overview:看到当前运行状态,加载使用的各种文件(日志,数据)
- connections:显示程序连接,显示来源的ip地址
- channels:基于某个或某几个connection创建的短连接
- exchanges:交换机的对象
- 默认每个用户一旦绑定了virTualHost就会创建7个默认的交
换机对象,2个路由,2个topic 2个headers 1 fanout;默认的
路由交换机AMQP default:所有的队列生成是自动绑定
- 默认每个用户一旦绑定了virTualHost就会创建7个默认的交
- queue:队列内容,包括显示消息数量,处理数量,可处理数量,未处理数量,总数量,可以拿到不同的队列中的消息内容;
- admin:对操作rabbitmq的用户权限做管理,自定义用户密码登录
添加用户
用户角色
- 超级管理员(administrator)
可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。 - 监控者(monitoring)
可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等) - 策略制定者(policymaker)
可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。 - 普通管理者(management)
仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。 - 其他
无法登陆管理控制台,通常就是普通的生产者和消费者。
创建Virtual Hosts
rabbitmq中每一个用户如果登录后想要操作资源(交换机,队列等)需要绑定一个有权限的虚拟主机virtualHost,每个虚拟主机都会管理一批不同的资源,之间是互相隔离的;
看到权限已加:
五种工作模式
简单模式
生产端:绑定rabbitmq默认交换机,交换机名称"",接收消息后根据消息中携带的路由key(队列名称) 找到绑定队列发送消息,如果交
换机找不到对应名称的队列,以垃圾数据处理,扔到垃圾桶,强调的是一发一收
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
定义连接工厂
public class ConnectionUtil {
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("IP地址");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("配置的host");
factory.setUsername("用户名");
factory.setPassword("密码");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
控制层
@ResponseBody
@GetMapping(value = "/mqsimple")
public String simpleMq() {
return rabbitService.simpleMq();
}
服务层
服务层
AtomicInteger integer = new AtomicInteger();
// 队列名
private final static String QUEUE_NAME = "hello";
// 创建一个连接
// @Autowired
@Autowired
ThreadPoolExecutor executor;
//假装同时有一百次处理
public String simpleMq() {
for (int i = 0; i < 100; i++) {
executor.execute(new RunnableRabbit());
}
//不要executor.shutdown(),因为提交了任务,线程池关闭了,实际上任务放在队列中未执行完毕,正真执行会报错
// executor.shutdown();
return "发送成功,不用等到结果就能继续执行";
}
private class RunnableRabbit implements Runnable {
Channel channel;
//发送消息
@Override
public void run() {
// 创建一个通道
try {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个队列
// queueDeclare(String queue, boolean durable, boolean exclusive, boolean
// autoDelete, Map<String, Object> arguments)
// 参数1 queue :队列名
// 参数2 durable :是否持久化
// 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除
// 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列
// 参数5 arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 发送消息
String message = "Hello World!";
// basicPublish(String exchange, String routingKey, BasicProperties props,
// byte[] body)
// 参数1 exchange :交换器
// 参数2 routingKey : 路由键
// 参数3 props : 消息的其他参数
// 参数4 body : 消息体
// 简单模式交换机为“”,路由键默认是队列名称
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
//每次休眠0.5秒
Thread.sleep(500);
integer.incrementAndGet();
System.out.println(" [x] Sent '" + message + integer);
//工具类用来关闭通道和连接
RabbitUtil.closeRabbit(channel, connection);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
}
//关闭通道和长连接
public class RabbitUtil {
public static void closeRabbit(Channel channel, Connection connection) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
channel = null;
} finally {
try {
connection.close();
} catch (IOException e) {
connection = null;
}
}
}
}
执行http://localhost:8093/mqsimple
会立即返回页面信息
-----------------------------------------------------——————美丽的分割线————————---------------------------------------------------------------------
声明队列是幂等的, 队列只会在它不存在时才会被创建,多次声明并不会重复创建。消息内容是一个字节数组,也就意味着可以传递任何数据。所以有300个消息放进了队列且只有一个队列无论发送多少条信息
点击队列名
可以看到每一条发送的信息
队列状态
使用的默认交换机,路由键为队列名
目前队列有三百个信息等待处理
消费方
控制层
@GetMapping(value = "/recv")
public void recv() {
rabbitService.recv();
}
服务层
public void recv() {
AtomicInteger integer = new AtomicInteger();
try {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个队列
// queueDeclare(String queue, boolean durable, boolean exclusive, boolean
// autoDelete, Map<String, Object> arguments)
// 参数1 queue :队列名
// 参数2 durable :是否持久化
// 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除
// 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列
// 参数5 arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
while (true) {
// 创建队列消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws UnsupportedEncodingException {
String message = new String(body, "UTF-8");
integer.incrementAndGet();
System.out.println(" [x] Received '" + message+integer);
}
};
// basicConsume(String queue, boolean autoAck, Consumer callback)
// 参数1 queue :队列名
// 参数2 autoAck : 是否自动ACK
// 参数3 callback : 消费者对象的一个接口,用来配置回调
channel.basicConsume(QUEUE_NAME, true, consumer);
}
} catch (Exception e) {
e.printStackTrace();
}
}
执行http://localhost:8093/recv
控制台显示300个信息已经被消费
消息已经被处理完毕
工作队列
工作队列,又称任务队列,主要思想是避免立即执行资源密集型任务,并且必须等待完成。相反地,我们进行任务调度,我们将一个任务封装成一个消息,并将其发送到队列。工作进行在后台运行不断的从队列中取出任务然后执行。当你运行了多个工作进程时,这些任务队列中的任务将会被工作进程共享执行。 这个概念在 Web 应用程序中特别有用,在短时间 HTTP 请求内需要执行复杂的任务。
编写两个消费者
//工作模式
public void work1() throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 创建队列消费者
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws UnsupportedEncodingException {
String message = new String(body, "UTF-8");
integer2.incrementAndGet();
System.out.println(" [x] Received '" + message + "工人one" + integer2);
}
};
// 消息应答关闭
boolean autoAck = true;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
public void work2() throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 创建队列消费者
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws UnsupportedEncodingException {
String message = new String(body, "UTF-8");
integer2.incrementAndGet();
System.out.println(" [x] Received '" + message + "工人two" + integer2);
}
};
// 消息应答关闭
boolean autoAck = true;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
控制器代码略,通过前台启动两个消费者
在启动发送者
结果是两个消费者会交替一个一个执行,因为在一个控制台所以会有错乱(反正是我的笔记博客),通过增加更多的工作程序就可以进行并行工作,并且接受的消息平均分配。注意的是,这种分配过程是一次性分配,并非一个一个分配。 默认情况下,RabbitMQ 将会发送的每一条消息按照顺序给下一个消费者。平均每一个消费者将获得相同数量的消息。这种分发消息的方式叫做轮询调度。
消息应答(Message acknowledgment)
当前代码会存在一个问题,一旦 RabbitMQ 向客户发送消息,它立即将其从内存中删除。在这种情况下,如果某个工作者还没执行完工作就死掉了,那么就会丢失信息, 但是,我们不想失去任何消息。如果某个工作进程被杀死时,我们希望把这个任务交给另一个工作进程。 为了确保消息永远不会丢失,RabbitMQ 支持消息应答。从消费者发送一个确认信息告诉 RabbitMQ 已经收到消息并已经被接收和处理,然后RabbitMQ 可以自由删除它。 如果消费者被杀死而没有发送应答,RabbitMQ 会认为该信息没有被完全的处理,然后将会重新转发给别的消费者。如果同时有其他消费者在线,则会迅速将其重新提供给另一个消费者。这样就可以确保没有消息丢失,即使工作进程偶尔也会死亡。 默认情况下,消息应答是开启的。在前面的例子中,通过 autoAck = true 标志明确地将它们关闭。现在是一旦完成任务,将此标志设置为false ,并向工作进程发送正确的确认。
发送端
修改消费者代码
public void work1() throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 创建队列消费者
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws UnsupportedEncodingException {
String message = new String(body, "UTF-8");
integer2.incrementAndGet();
System.out.println(" [x] Received '" + message + "工人one" + integer2);
// 每次处理完成一个消息后,手动发送一次应答。
try {
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (IOException e) {
e.printStackTrace();
}
}
};
// 消息应答关闭
// boolean autoAck = true;
// 消息应答关开启
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
其中,首先关闭自动应答机制。
// 消息应答关闭
// boolean autoAck = true;
// 消息应答关开启
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
然后,每次处理完成一个消息后,手动发送一次应答。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
这样消费者每处理一条信息就会应答给生成者,生产者就会删除信息,如果没有应答,生成者就会重新发送给可用的消费者
公平转发(Fair dispatch)
但是目前的代码还是有问题,调度仍然无法正常工作。例如在两个工作线程的情况下,一个工作线程将不断忙碌,另一个工作人员几乎不会做任何工作。那么,RabbitMQ 不知道什么情况,还会平均分配消息。 这是因为当消息进入队列时,RabbitMQ 只会分派消息。它不看消费者的未确认消息的数量。它只是盲目地向第 n 个消费者发送每个第 n 个消息。 为了解决这样的问题,我们可以使用 basicQos 方法,并将传递参数为 prefetchCount = 1。 这样告诉 RabbitMQ 不要一次给一个工作线程多个消息。换句话说,在处理并确认前一个消息之前,不要向工作线程发送新消息。相反,它将发送到下一个还不忙的工作线程。
修改生产者代码如下:
try {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个队列
// queueDeclare(String queue, boolean durable, boolean exclusive, boolean
// autoDelete, Map<String, Object> arguments)
// 参数1 queue :队列名
// 参数2 durable :是否持久化
// 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除
// 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列
// 参数5 arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 公平转发
int prefetchCount = 1;
channel.basicQos(prefetchCount) ;
// 发送消息
String message = "Hello World!";
// basicPublish(String exchange, String routingKey, BasicProperties props,
// byte[] body)
// 参数1 exchange :交换器
// 参数2 routingKey : 路由键
// 参数3 props : 消息的其他参数
// 参数4 body : 消息体
// 简单模式交换机为“”,路由键默认是队列名称
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
integer.incrementAndGet();
System.out.println(" [x] Sent '" + message + integer);
// 工具类用来关闭通道和连接
RabbitUtil.closeRabbit(channel, connection);
} catch (Exception e) {
e.printStackTrace();
}
其中,使用 basicQos 方法,并将传递参数为 prefetchCount = 1。
int prefetchCount = 1;
channel.basicQos(prefetchCount) ;
消息持久化(Message durability)
现在确保即使消费者死了,任务也不会丢失。但是如果 RabbitMQ 服务器停止,任务仍然会丢失。 当 RabbitMQ 退出或崩溃时,将会丢失所有的队列和信息,除非告诉它不要丢失。需要两件事来确保消息不丢失:需要分别将队列和消息标记为持久化。 首先,需要确保 RabbitMQ 永远不会失去队列。为了这样做,需要将其声明为持久化的。
再次修改生产者代码:
// 创建一个通道
try {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个队列
// queueDeclare(String queue, boolean durable, boolean exclusive, boolean
// autoDelete, Map<String, Object> arguments)
// 参数1 queue :队列名
// 参数2 durable :是否持久化
// 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除
// 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列
// 参数5 arguments
//持久化队列参数
boolean durable = true;
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
// 公平转发
int prefetchCount = 1;
channel.basicQos(prefetchCount) ;
// 发送消息
String message = "Hello World!";
// basicPublish(String exchange, String routingKey, BasicProperties props,
// byte[] body)
// 参数1 exchange :交换器
// 参数2 routingKey : 路由键
// 参数3 props : 消息的其他参数
// 参数4 body : 消息体
// 简单模式交换机为“”,路由键默认是队列名称
//MessageProperties.PERSISTENT_TEXT_PLAIN----消息持久化
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
integer.incrementAndGet();
System.out.println(" [x] Sent '" + message + integer);
// 工具类用来关闭通道和连接
RabbitUtil.closeRabbit(channel, connection);
} catch (Exception e) {
e.printStackTrace();
}
消费端
// 指定一个队列,第二个参数为持久化队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
现在我们先启动生成者然后关闭rabbitmq服务器记得之前要换个队列名,换个队列玩,不然会报错
现在我们重启服务器
启动一个消费者后:
9个小时后队列还在
发布订阅模式
解读:
- 1个生产者,多个消费者
- 每一个消费者都有自己的一个队列
- 生产者没有将消息直接发送到队列,而是发送到了交换机
- 每个队列都要绑定到交换机
- 生产者发送的消息,经过交换机,到达队列,实现,一个消息被多个消费者获取的目的注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
交换器一共四种类型:direct、topic、headers、fanout。目前,先关注 fanout 类型的交换器。
channel.exchangeDeclare(“logs”, “fanout”);
fanout 类型的交换器非常简单,它只是将所有收到的消息广播到所有它所知道的队列。不知道交换器,但仍然能够发送消息到队列。这是因为使用了一个默认的交换器,用空的字符串(“”)。
// 参数1 exchange :交换器
// 参数2 routingKey : 路由键
// 参数3 props : 消息的其他参数
// 参数4 body : 消息体
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
其中,第一个参数表示交换器的名称,我们设置为””,第二个参数表示消息由路由键决定发送到哪个队列。 现在,我们可以发布消息到我们命名的交换器。
channel.basicPublish("logs", "", null, message.getBytes());
```
**临时队列(Temporary queues)**
之前,都是使用的队列指定了一个特定的名称。不过现在并不关心队列的名称。想要接收到所有的消息,而且也只对当前正在传递的消息感兴趣。要解决需求,需要做两件事。 首先,每当连接到 RabbitMQ,我们需要一个新的空的队列。为此,可以创建一个具有随机名称的队列,或者甚至更好 - 让服务器或者,让服务器为选择一个随机队列名称。 其次,一旦消费者与 RabbitMQ 断开,消费者所接收的那个队列应该被自动删除。 在 Java 客户端中,可以使用 queueDeclare() 方法来创建一个非持久的、唯一的、自动删除的队列,且队列名称由服务器随机产生。
```java
String queueName = channel.queueDeclare().getQueue();
此时,queueName 包含一个随机队列名称。
绑定(Bindings)
已经创建了一个 fanout 类型的交换器和队列。现在,需要告诉交换器发送消息到我们的队列。交换器和队列之间的关系称为绑定
// 绑定交换器和队列
// 参数1 queue :队列名
// 参数2 exchange :交换器名
// 参数3 routingKey :路由键名
channel.queueBind(queueName, "logs", "");
代码演示:
发送端
public String pubor() throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个交换器
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 发送消息
String message = "Liang-MSG log.";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
return "发布成功请订阅";
}
接收端
public String puborw1() throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个交换器
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 创建一个非持久的、唯一的、自动删除的队列
String queueName = channel.queueDeclare().getQueue();
// 绑定交换器和队列
// queueBind(String queue, String exchange, String routingKey)
// 参数1 queue :队列名
// 参数2 exchange :交换器名
// 参数3 routingKey :路由键名
channel.queueBind(queueName, EXCHANGE_NAME, "");
// 创建队列消费者
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received work1 '" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
return "订阅成功";
}
public String puborw2() throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 指定一个交换器
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 创建一个非持久的、唯一的、自动删除的队列
String queueName = channel.queueDeclare().getQueue();
// 绑定交换器和队列
// queueBind(String queue, String exchange, String routingKey)
// 参数1 queue :队列名
// 参数2 exchange :交换器名
// 参数3 routingKey :路由键名
channel.queueBind(queueName, EXCHANGE_NAME, "");
// 创建队列消费者
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received work2'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
return "订阅成功";
}
先启动两个接收端,然后启动发送端
几乎所有接收端同时获得并处理相同的消息,直就是发布与订阅,发布给已经订阅的队列,交换机通过路由键确定发送到哪个队列
路由模式
直接交换(Direct exchange)是通过直接交换机实现的,发送端将消息发给交换机,通过路由键路要到相应的队列,
其中,第一个队列与绑定键 orange 绑定,第二个队列有两个绑定,一个绑定键为 black,另一个绑定为 green。在这样的设置中,具有 orange 的交换器的消息将被路由到队列 Q1。具有 black 或 green 的交换器的消息将转到 Q2。所有其他消息将被丢弃。
多重绑定(Multiple bindings)
此外,使用相同的绑定键绑定多个队列是完全合法的。在我们的示例中,可以在 X 和 Q1 之间添加绑定键 black。在这种情况下,direct 类型的交换器将消息广播到所有匹配的队列 Q1 和 Q2。
代码演示:
发送端
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_logs";
private static final String[] LOG_LEVEL_ARR = {"debug", "info", "error"};
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 指定一个交换器
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 发送消息
for (int i = 0; i < 10; i++) {
int rand = new Random().nextInt(3);
String severity = LOG_LEVEL_ARR[rand];
String message = "Liang-MSG log : [" +severity+ "]" + UUID.randomUUID().toString();
// 发布消息至交换器
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
// 关闭频道和连接
channel.close();
connection.close();
}
}
接收端
public class ReceiveLogsDirect {
private static final String EXCHANGE_NAME = "direct_logs";
private static final String[] LOG_LEVEL_ARR = {"debug", "info", "error"};
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 指定一个交换器
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 设置日志级别
int rand = new Random().nextInt(3);
String severity = LOG_LEVEL_ARR[rand];
// 创建一个非持久的、唯一的、自动删除的队列
String queueName = channel.queueDeclare().getQueue();
// 绑定交换器和队列
// queueBind(String queue, String exchange, String routingKey)
// 参数1 queue :队列名
// 参数2 exchange :交换器名
// 参数3 routingKey :路由键名
channel.queueBind(queueName, EXCHANGE_NAME, severity);
// 创建队列消费者
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
现在,做一个实验,开启三个 ReceiveLogsDirect 工作程序:ReceiveLogsDirect1 、ReceiveLogsDirect2 与 ReceiveLogsDirect3。
ReceiveLogsDirect1
[*] Waiting for messages. To exit press CTRL+C
[*] LOG LEVEL : info
[x] Received 'Liang-MSG log : [info]0dd0ae0c-bf74-4aa9-9852-394e65fbf939'
[x] Received 'Liang-MSG log : [info]b2b032f6-e907-4c95-b676-1790204c5f73'
[x] Received 'Liang-MSG log : [info]14482461-e432-4866-9eb5-a28f4edeb47f'
ReceiveLogsDirect2
[*] Waiting for messages. To exit press CTRL+C
[*] LOG LEVEL : error
[x] Received 'Liang-MSG log : [error]493dce2a-7ce1-4111-953c-99ab2564a2d0'
[x] Received 'Liang-MSG log : [error]2446dd80-d5f0-4d39-888f-31579b9d2724'
[x] Received 'Liang-MSG log : [error]fe8219e0-5548-40ba-9810-d922d1b03dd8'
[x] Received 'Liang-MSG log : [error]797b6d0e-9928-4505-9c76-56043322b1f0'
ReceiveLogsDirect3
[*] Waiting for messages. To exit press CTRL+C
[*] LOG LEVEL : debug
[x] Received 'Liang-MSG log : [debug]c05eee3e-b820-4b69-9c3f-c2bbded85195'
[x] Received 'Liang-MSG log : [debug]4645c9ba-4070-41d7-adc9-7f8b2df1e3c8'
[x] Received 'Liang-MSG log : [debug]d3d3ad5c-8f97-49ea-8fd6-c434790e40eb'
此时,ReceiveLogsDirect1 、ReceiveLogsDirect2 与 ReceiveLogsDirect3 同时收到了属于自己级别的消息。
主题模式(通配符模式)
主题交换(Topic exchange)
使用 topic 类型的交换器,不能有任意的绑定键,它必须是由点隔开的一系列的标识符组成。标识符可以是任何东西,但通常它们指定与消息相关联的一些功能。其中,有几个有效的绑定键,例如 “stock.usd.nyse”,“nyse.vmw”, “quick.orange.rabbit”。可以有任何数量的标识符,最多可达 255 个字节。 topic 类型的交换器和 direct 类型的交换器很类似,一个特定路由的消息将被传递到与匹配的绑定键绑定的匹配的所有队列。关于绑定键有两种有两个重要的特殊情况:
* 可以匹配一个标识符。
# 可以匹配零个或多个标识符。
消息将使用由三个字(两个点)组成的绑定键发送。绑定键中的第一个字将描述速度,第二个颜色和第三个种类:“…”。其中, Q1 对所有的橙色动物感兴趣。而 Q2 想听听有关兔子的一切,以及关于懒惰动物的一切。 如果我们违反合同并发送一个或四个字的消息,如 “quick.orange.male.rabbit” 会发生什么?那么,这些消息将不会匹配任何绑定,并将被丢失。 topic 类型的交换器是强大的,可以实现其他类型的交换器。 当一个队列与“#”绑定绑定键时,它将接收所有消息,类似 fanout 类型的交换器。 当一个队列与 “*” 和 “#” 在绑定中不被使用时,类似 direct 类型的交换器。
同一个消息被多个消费者获取。一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。
代码演示
发送端
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
private static final String[] LOG_LEVEL_ARR = {"dao.debug", "dao.info", "dao.error",
"service.debug", "service.info", "service.error",
"controller.debug", "controller.info", "controller.error"};
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 指定一个交换器
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 发送消息
for (String severity : LOG_LEVEL_ARR) {
String message = "Liang-MSG log : [" +severity+ "]" + UUID.randomUUID().toString();
// 发布消息至交换器
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
// 关闭频道和连接
channel.close();
connection.close();
}
}
接收端
public class ReceiveLogsTopic {
private static final String EXCHANGE_NAME = "topic_logs";
private static final String[] LOG_LEVEL_ARR = {"#", "dao.error", "*.error", "dao.*", "service.#", "*.controller.#"};
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设置 RabbitMQ 的主机名
factory.setHost("localhost");
// 创建一个连接
Connection connection = factory.newConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 指定一个交换器
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 设置日志级别
int rand = new Random().nextInt(5);
String severity = LOG_LEVEL_ARR[rand];
// 创建一个非持久的、唯一的、自动删除的队列
String queueName = channel.queueDeclare().getQueue();
// 绑定交换器和队列
channel.queueBind(queueName, EXCHANGE_NAME, severity);
// 打印
System.out.println(" [*] LOG INFO : " + severity);
// 创建队列消费者
final Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
接收端只会匹配到相应的键才会消费,没有被匹配的键的消息将会被丢弃