消息队列
什么是消息队列
把服务器比喻成一个人
当这个人的亲人,朋友,工作(一大批客户端)【生产者】同时有事情(网络请求)找他, 这个人承受不了了 就容易崩溃
那现在可以把消息队列比喻成一个管道 这个管道让消息按照队列数据结构(先进先出)
然后这个人再把这些消息传给可以解决的人【消费者】
消息队列的优势
应用解耦
消息队列可以使消费者和生产者直接互不干涉,互不影响,只需要把消息发送到队列即可,而且可独立的扩展或修改两边的处理过程,只要能确保它们遵守同样的接口约定,可以生产者用Node.js实现,消费者用python实现。
灵活性和峰值处理能力
当客户端访问量突然剧增,对服务器的访问已经超过服务所能处理的最大峰值,甚至导致服务器超时负载崩溃,使用消息队列可以解决这个问题,可以通过控制消费者的处理速度
和生产者可进入消息队列的数量
等来避免峰值问题
排序保证
消息队列可以控制数据处理的顺序,因为消息队列本身使用的是队列这个数据结构,FIFO
(先进选出),在一些场景数据处理的顺序很重要,比如商品下单顺序等。
异步通信
消息队列中的有些消息,并不需要立即处理,消息队列提供了异步处理机制,可以把消息放在队列中并不立即处理,需要的时候处理,或者异步慢慢处理,一些不重要的发送短信和邮箱功能可以使用。
可扩展性
前面提到了消息队列可以做到解耦
,如果我们想增强消息入队和出队的处理频率,很简单,并不需要改变代码中任何内容,可以直接对消息队列修改一些配置即可,比如我们想限制每次发送给消费者的消息条数等。
主流的消息队列
Kafka:高吞吐量的分布式发布订阅消息系统,支持单机每秒百万并发。用于日志(消息)系统
RocketMQ:低延迟、高可靠、可伸缩、易于使用的消息中间件,思路起源于 Kafka。最大的问题商业版收费,有些功能不开放。
RabbitMQ:基于 AMQP 协议的开源消息队列系统。能保证消息的可靠性、稳定性、安全性。高并发
的特性,毋庸置疑,RabbitMQ 最高,原因是它的实现语言是天生具备高并发高可用的erlang 语言,天生的分布式
优势。
rabbitmq安装
mac:
brew install rabbitmq
/usr/local/Cellar/rabbitmq/3.7.8
sbin/rabbitmq-server
浏览器输入 http://localhost:15672/#/ 默认用户名密码 guest
通信默认端口号 15672:管理控制台默认端口号 25672:集群通信端口号 注意: 阿里云 ECS 服务器如果出现 RabbitMQ 安装成功,外网不能访问是因为安全组的问题没有开放端口
- whereis rabbitmq:查看 rabbitmq 安装位置
- rabbitmqctl start_app:启动应用
- whereis erlang:查看erlang安装位置
- rabbitmqctl start_app:启动应用
- rabbitmqctl stop_app:关闭应用
- rabbitmqctl status:节点状态
- rabbitmqctl add_user username password:添加用户
- rabbitmqctl list_users:列出所有用户
- rabbitmqctl delete_user username:删除用户
- rabbitmqctl add_vhost vhostpath:创建虚拟主机
- rabbitmqctl list_vhosts:列出所有虚拟主机
- rabbitmqctl list_queues:查看所有队列
- rabbitmqctl -p vhostpath purge_queue blue:清除队列里消息
以上终端所有命令,需要进入到rabbitmqctl的sbin目录下执行rabbitmqctl命令才有用,否则会报错
- 生产者 :生产消息的
- 消费者 :接收消息的
- 通道 channel:建立连接后,会获取一个 channel 通道
- exchange :交换机,消息需要先发送到 exchange 交换机,也可以说是第一步存储消息的地方(交换机会有很多类型,后面会详细说)。
- 消息队列 : 到达消费者前一刻存储消息的地方,exchange 交换机会把消息传递到此
- ack回执:收到消息后确认消息已经消费的应答
npm install amqplib
生产者: product.js
const amqp =require('amqplib');
async function product(params) {
// 1. 创建链接对象
const connection = await amqp.connect('amqp://localhost:5672');
// 2. 获取通道
const channel = await connection.createChannel();
// 3. 声明参数
const routingKey = 'helloKoalaQueue';
const msg = 'hello koala';
for (let i=0; i<10000; i++) {
// 4. 发送消息
await channel.publish('', routingKey, Buffer.from(`${msg} 第${i}条消息`));
}
// 5. 关闭通道
await channel.close();
// 6. 关闭连接
await connect.close();
}
product();
消费者代码 consumer.js
// 构建消费者
const amqp = require('amqplib');
async function consumer() {
// 1. 创建链接对象
const connection = await amqp.connect('amqp://localhost:5672');
// 2. 获取通道
const channel = await connection.createChannel();
// 3. 声明参数
const queueName = 'helloKoalaQueue';
// 4. 声明队列,交换机默认为 AMQP default
await channel.assertQueue(queueName);
// 5. 消费
await channel.consume(queueName, msg => {
console.log('Consumer:', msg.content.toString());
channel.ack(msg);
});
}
consumer();
生产者发消息的时候必须指定一个 exchange,否则消息无法直接到达消息队列,Exchange将消息路由到一个或多个Queue中(或者丢弃)
交换机的种类
常用的四种类型
-
fanout: Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,不需要设置路由键。
-
direct:direct 把消息路由到那些 binding key与 routing key 完全匹配的 Queue中。
-
topic:生产者指定 RoutingKey 消息根据消费端指定的队列通过模糊匹配的方式进行相应转发,两种通配符模式: #:可匹配一个或多个关键字 *:只能匹配一个关键字
-
headers:和主题交换机有点相似,但是不同于主题交换机的路由是基于路由键,头交换机的路由值基于消息的 header 数据。 主题交换机路由键只有是字符串,而头交换机可以是整型和哈希值 header Exchange 类型用的比较少,可以自行 google 了解。
不管是哪一种类型的交换机,都有一个绑定binding的操作,只不过根据不同的交换机类型有不同的路由绑定策略。
fanout: 所有消息都会路由到两个Queue中,是两个消费者都可以收到全部的完全相同的消息吗? 答案是的,两个消费者收到的队列消息正常应该是完全相同的。这种类型常用于广播类型的需求,或者也可以消费者1记录日志 ,消费者2打印日志
生产者:
const amqp = require('amqplib');
async function producer() {
// 创建链接对象
const connection = await amqp.connect('amqp://localhost:5672');
// 获取通道
const channel = await connection.createChannel();
// 声明参数
const exchangeName = 'fanout_koala_exchange';
const routingKey = '';
const msg = 'hello koala';
// 交换机
await channel.assertExchange(exchangeName, 'fanout', {
durable: true,
});
// 发送消息
await channel.publish(exchangeName, routingKey, Buffer.from(msg));
// 关闭链接
await channel.close();
await connection.close();
}
producer();
消费者:
const amqp = require('amqplib');
async function consumer() {
// 创建链接对象
const connection = await amqp.connect('amqp://localhost:5672');
// 获取通道
const channel = await connection.createChannel();
// 声明参数
const exchangeName = 'fanout_koala_exchange';
const queueName = 'fanout_kaola_queue';
const routingKey = '';
// 声明一个交换机
await channel.assertExchange(exchangeName, 'fanout', { durable: true });
// 声明一个队列
await channel.assertQueue(queueName);
// 绑定关系(队列、交换机、路由键)
await channel.bindQueue(queueName, exchangeName, routingKey);
// 消费
await channel.consume(queueName, msg => {
console.log('Consumer:', msg.content.toString());
channel.ack(msg);
});
console.log('消费端启动成功!');
}
consumer();