RabbitMQ
开始
学习的话可以尝试去官网看教程,这个是最官方,最权威的。
我们首先进入官网(https://www.rabbitmq.com/) ,点击Started。
这里选择Java。
AMQP协议
AMQP协议
RabbitMQ是遵守AMQP协议(Advanced Message Queuing Protocol)高级消息队列协议
具体流程:发布者(publisher)将消息发送给交换机(exchange),然后交换机将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。
总结一下:这些都是AMQP实体。
发布者:发消息到交换机
交换机:负责把消息 转发 到相应的队列
队列:存储消息
消费者:从某个队列取出消息
AMQP包含一个消息确认(message acknowledgements)的概念:当一个消息从队列中投递给消费者后,消费者会通知消息代理消费情况,这个可以是自动的,也可以由处理消息的应用的开发者执行。当“消息确认”被启用时,消息代理直到收到来自消费者的确认回执(acknowledgement)才完全删除队列中的消息。
安装
https://www.rabbitmq.com/docs/download
注意一下还要安装erlang,这也是必备的环境。
安装完成后,可以在查看一下service服务,看是否启动了RabbitMQ
然后http://localhost:15672/就可以进入管理面板,账号密码都是guest,然后就可以进入到控制面板了。
注意:如果你打算在自己的服务器上部署 RabbitMQ,在默认设置下,使用 guest 账号是无法访问的。为何会这样呢?
原因在于系统安全性的需求。比如,攻击者可能会扫描你服务器的端口,并不断尝试用guest 账号登录。一旦找到一个开放的端口并用 guest 登录,攻击者就可以入侵你的服务器,然后在消息队列中不断添加消息,填满你的服务器硬盘。因此,为了安全,系统默认关闭了 quest 账号的远程访问权限,这也是官方出于安全。
总共有六种
1、一对一模型
看图可以得出一个生产者给一个队列发消息,一个消费者从队列里取出消息。
可以去maven仓库找到对应的依赖,在xml里配置。
然后查看实例代码,网页滑到最下面
点进github链接,复制代码
生产者代码:
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 创建队列名称
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
// 发送消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
queueDeclare参数:
- queueName:消息队列名称(注意,同名称的消息队列,只能用同样的参数创建一次)
- durabale:消息队列重启后,消息是否丢失
- exclusive:是否只允许当前这个创建消息队列的连接操作消息队列
- autoDelete:没有人用队列后,是否要删除队列
点击运行,发送一条Hello World消息:
控制面板可以发现一个hello队列,并且有一条消息:
消费者代码:
public class Consumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 创建消息队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 有消息过来就会回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
// 发送消息
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
运行后,发现消费者接收到了Hello World消息,控制面板也可以看出来。
2、一对多消费者
这里消息队列的模型为:一个生产者给一个队列发消息,多个消费者从这个队列取消息(1 对多)。
适用场景:假设我们有一个消费者,由于其性能有限,无法处理所有任务。此时,我们可以增加机器来提高处理能力。假设一个生产者在不断地生成任务,但一个消费者无法全部处理,那么可以增加消费者来共同完成这些任务。
因此,这种场景特别适合于多个机器同时接收并处理任务,尤其是在每个机器的处理能力有限的情况下。
同理,找到对应的代码。
生产者代码:
改动了一下,可以自己控制发消息
public class MultiProducer {
private static final String TASK_QUEUE_NAME = "multi";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 消息队列持久化
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.nextLine();
// 消息持久化
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
idea控制台发送消息
RabbitMQ控制台可以发现有2条消息
消费者代码:
public class MultiConsumer {
private static final String TASK_QUEUE_NAME = "multi";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
for (int i = 0; i < 2; i++) {
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 一个队列最多同时接受1个未确认的消息
channel.basicQos(1);
// 如何处理消息
int finalI = i;
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
try {
// 处理工作
System.out.println(" [x] Received '" + "编号" + finalI + "的队列接受到了" + message + "'");
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println(" [x] Done");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
}
}
创建了2个消费者,同时执行了消息队列的“fist message”和"second message”。
3、交换机
什么是交换机?
交换机是消息队列中的一个组件,其作用类似于网络路由器。它负责将我们发送的消息转发到不同的队列上。交换机的主要功能是根据消息的路由规则将消息投递到合适的队列或绑定的消费者。
简单来说就是将一个生产者给多个队列发消息(1 个生产者对多个队列)。
类似这样的模型,图片转自编程导航。
生成者代码:
public class FanoutProducer {
private static final String EXCHANGE_NAME = "fanout-exchange";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 创建交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.nextLine();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
消费者代码:
public class FanoutConsumer {
private static final String EXCHANGE_NAME = "fanout-exchange";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
Channel channel2 = connection.createChannel();
// 声明交换机
channel1.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 创建队列,随机分配一个队列名称
String queueName = "xiaowang_queue";
channel1.queueDeclare(queueName, true, false, false, null);
channel1.queueBind(queueName, EXCHANGE_NAME, "");
String queueName2 = "xiaoli_queue";
channel2.queueDeclare(queueName2, true, false, false, null);
channel2.queueBind(queueName2, EXCHANGE_NAME, "");
channel2.queueBind(queueName2, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小王] Received '" + message + "'");
};
DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小李] Received '" + message + "'");
};
channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> { });
channel2.basicConsume(queueName2, true, deliverCallback2, consumerTag -> { });
}
}
查看队列里有2个
4、路由
特点:消息会根据路由键转发到指定的队列
场景举例:上一个案例,老板发布一个任务,所有员工都能收到。但现在,我们有小王的任务队列和小李的任务队列。然而,老板只想给小王发送任务,我们只需将任务发送到小王的队列,fanout 交换机无法满足需求,这时,我们可以使用 direct 交换机。通过建立一个绑定关系如果老板的任务是给小王的,它会被发送到小王的队列。从系统的角度来看,相当于这个消息是给A系统处理任务的,而不是给其他的系统。
再举个例子,发日志的场景,希望用独立的程序来处理不同级别的日志,比如C1 系统处理 error 日志,C2 系统处理其他级别
的日志。就是下面的图。
生产者代码:
public class DirectProducer {
private static final String EXCHANGE_NAME = "direct-exchange";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String userInput = scanner.nextLine();
String[] strings = userInput.split(" ");
if (strings.length < 1) {
continue;
}
String message = strings[0];
String routingKey = strings[1];
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
}
}
}
//..
}
消费者代码:
public class DirectConsumer {
private static final String EXCHANGE_NAME = "direct-exchange";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
// 创建队列,随机分配一个队列名称
String queueName = "xiaowang_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "xiaowang");
// 创建队列,随机分配一个队列名称
String queueName2 = "xiaoli_queue";
channel.queueDeclare(queueName2, true, false, false, null);
channel.queueBind(queueName2, EXCHANGE_NAME, "xiaoli");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback xiaowangDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaowang] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaoliDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoli] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, xiaowangDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, true, xiaoliDeliverCallback, consumerTag -> {
});
}
}
进入RabbitMQ控制面板,点击交换机,可以发现有两个队列,分别绑定了对应的规则,发送不同的消息就可以转发到对应的队列。
发送消息给xiaowang,idea控制台可以看到给xiaowang队列发送了message1消息
xiaowang队列接收到了message1消息
5、topic交换机
特点:消息会根据一个模糊的路由键转发到指定的队列
场景:特定的一类消息可以交给特定的一类系统(程序)来处理绑定关系:可以模糊匹配多个绑定
匹配一个单词,比如*.orange,那么 a.orange、b.orange 都能匹配#:匹配0个或多个单词,比如 a.#,那么 a.a、a.b、a.a.c 都能匹配
生产者代码:
public class TtlProducer {
private final static String QUEUE_NAME = "ttl_queue";
public static void main(String[] argv) throws Exception {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
// 建立连接、创建频道
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 发送消息
String message = "Hello World!";
// 给消息指定过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("1000")
.build();
channel.basicPublish("my-exchange", "routing-key", properties, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
消费者代码:
public class TopicConsumer {
private static final String EXCHANGE_NAME = "topic-exchange";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 创建队列
String queueName = "frontend_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "#.前端.#");
// 创建队列
String queueName2 = "backend_queue";
channel.queueDeclare(queueName2, true, false, false, null);
channel.queueBind(queueName2, EXCHANGE_NAME, "#.后端.#");
// 创建队列
String queueName3 = "product_queue";
channel.queueDeclare(queueName3, true, false, false, null);
channel.queueBind(queueName3, EXCHANGE_NAME, "#.产品.#");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback xiaoaDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoa] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaobDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaob] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaocDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoc] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, true, xiaoaDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, true, xiaobDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName3, true, xiaocDeliverCallback, consumerTag -> {
});
}
}
可以看到绑定的规则。
发送消息“a.前端.后端.a”,队列都接收到了。很麻烦,所以设置路由规则时候要好好设计
RPC
支持用消息队列模拟RPC调用,但是一般没必要,直接用Dubbo等就好了。