简介
RabbitMQ是一种消息中间件,它能接收并发送消息。为方便理解,可以将消息中间件认为是邮局。用户只需要将邮件送到邮局,邮局就能将邮件运送到收件人手中,与此类似消息中间件能够接收用户发送的消息并将消息转发给接收者。
作用
- 通信解耦:在RabbitMQ中,消息生产者和消费者不需要感知彼此的存在,只需要与消息队列交互,降低了生产者和消费者的耦合程度。在微服务系统中有广泛的应用。
- 流量削峰:服务器访问量过于庞大时,可以使用RabbitMQ将请求暂时储存在消息队列中,之后服务器按一定的频率从队列中取出请求并处理,避免大量请求直接冲击服务器。
- 负载均衡:当系统中存在多个消费者时,可以使用消息中间件将消息分发给多个消费者处理,避免单个消费者过载提升系统的处理能力。
RabbitMQ涉及的对象
产生消息的用户称为生产者(Producer),用字母P表示:
消息存储在消息队列中,多个生产者可以往同一个对列发送消息;多个消费者也可以从同一个对列中获取数据。消息队列用以下符号表示:
接收消息的用户称为消费者(Consumer),用字母C表示:
补充:生产者,消费者,消息中间件不一定处于同一个服务器上。在大多数场景中他们都运行在不同的服务器上,某一个应用既可以是生产者也可以是消费者。
案例一
以下是一个C#代码编写的极简RabbitMQ实例,生产者发送字符串“Holl World”到消息队列中,消费者从消息队列中接收消息并显示。
前置条件:本地已安装RabbitMQ软件,生产者端和消费者端都安装了RabbitMQ依赖包。
Nuget下载依赖包:NuGet\Install-Package RabbitMQ.Client -Version 7.0.0-rc.4
1、生产者端代码
- 创建连接(connection)
var factory = new ConnectionFactory { HostName = "localhost" };
var connection = await factory.CreateConnectionAsync();
本地安装的RabbitMQ默认运行在localhost:5627端口,创建连接时指定参数HostName = "localhost"连接本地安装的RabbitMQ。若要连接远程的RabbitMQ,在此修改HostName的值。
- 创建信道(channel)
var channel = await connection.CreateChannelAsync();
信道对象包含了大多数操作API。
- 创建队列
await channel.QueueDeclareAsync(queue: "hello",durable: false,exclusive: false,autoDelete: false,arguments: null);
队列的名称为“hello”,其余参数保持默认值。
- 发送消息
const string message = "Hello World!";
var body = Encoding.UTF8.GetBytes(message);
await channel.BasicPublishAsync(exchange:string.Empty,routingKey:"hello", body);
将消息发送给指定队列“hello”,注意routingKey的值与队列的名称相同。
- 完整代码Producer.cs如下:
using RabbitMQ.Client;
using System.Text;
namespace Producer
{
internal class Program
{
static async Task Main(string[] args)
{
var factory = new ConnectionFactory { HostName = "localhost" };
var connection = await factory.CreateConnectionAsync();
var channel = await connection.CreateChannelAsync();
await channel.QueueDeclareAsync(queue: "hello",durable: false,exclusive: false,autoDelete: false,arguments: null);
const string message = "Hello World!";
var body = Encoding.UTF8.GetBytes(message);
await channel.BasicPublishAsync(exchange:string.Empty,routingKey:"hello", body);
Console.WriteLine($"Producer发送消息:{message}");
Console.ReadLine();
}
}
}
2、消费者端代码
- 创建连接和信道
var factory = new ConnectionFactory { HostName = "localhost" };
var connection = await factory.CreateConnectionAsync();
var channel = await connection.CreateChannelAsync();
- 创建队列
await channel.QueueDeclareAsync(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
消费者端可能比生产者端先执行,在消费者端重新声明队列可以保证消费者接收消息前队列存在。且消息队列是幂等的,多次声明不会对已存在的队列产生影响。
- 创建consumer对象并编写回调函数
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"Consumer接收信息:{message}");
};
编写处理消息的回调函数。并将回调函数合并到consumer对象的事件中。
- 为消息队列指定消费者对象
await channel.BasicConsumeAsync(queue: "hello",autoAck: true,consumer: consumer);
消费者对象接收到消息时会自动调用回调函数。
- 完整代码Consumer.cs如下:
namespace Consumer
{
internal class Program
{
static async Task Main(string[] args)
{
var factory = new ConnectionFactory { HostName = "localhost" };
var connection = await factory.CreateConnectionAsync();
var channel = await connection.CreateChannelAsync();
await channel.QueueDeclareAsync(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"Consumer接收信息:{message}");
};
await channel.BasicConsumeAsync(queue: "hello",autoAck: true,consumer: consumer);
Console.ReadLine();
}
}
}
补充:最新的RabbitMQ依赖包将方法全都优化为异步方法,因此上述使用了大量Async方法。
案例二
使用RabbitMQ实现 “发布/订阅” 模型,一个生产者同时给两个消费者发送不同的消息。
在案例一中我们实现了生产者直接发送消息给队列,队列又将消息被转发给了一个明确的消费者,实际上这并不是RabbitMQ列常见的工作模式。
一般情况下生产者并不会直接给消息队列发送消息,也不知道消息会被送到哪一个队列中。真实的情况是生产者将消息发送给交换机(Exchanges)。交换机就好比是邮递员,顾客寄邮件不需要知道邮局的地址,只需要将邮件交给上门取件的邮递员就能完成寄件。生产者只需要将消息送给交换机,后续由交换机决定接下来的操作。是将消息添加到一个特定的队列中?还是将添加到多个队列中?亦或是将消息丢弃?在声明交换机时可以通过ExchangeType参数配置。
交换机(Exchange)用字母X表示:
监听交换机
rabbitmqctl list_exchanges
在命令行窗口输入以上指令,查看RabbitMQ节点上的交换机。
补充:
- 使用rabbitmqctl指令前需要安装Erlang环境,并将RabbitMQ的sbin路径添加进环境变量中。
- 建议安装Erlang和RabbitMQ时采用默认的安装路径,否则可能出现本地erlang.cookie和RabbitMQ节点的erlang.cookie不一致,导致无法使用命令行指令。
- 自定义安装路径导致cookie校验无法通过时,可以尝试以下操作:
找到以下两个文件,用后者的cookie文件替换掉前者的cookie文件。
C:\Windows\System32\config\systemprofile\.erlang.cookie
C:\用户\你的用户名\.erlang.cookie。
- 默认交换机
在案例一中我们并未声明交换机依然可以将消息发送到队列中,是因为生产者在发送消息时使用了RabbitMQ服务器默认的交换机。上述以 amq.* 标识的交换机就是默认交换机。
await channel.BasicPublishAsync(exchange:string.Empty,routingKey:"hello", body);
第一个参数是交换机的名称,值为空串表示将消息发送给默认的交换机。再由默认的交换将消息发送给与routingKey同名的队列。
注意:routingKey只是值与队列名称相同,二者是不同的属性。
- 创建交换机
await channel.ExchangeDeclareAsync(exchange:"X", ExchangeType.Direct);
ExchangeType有4种类型
1、Direct:生产者发送消息使用的routingKey与交换机绑定队列使用的routingKey相同时,交换机才会发送消息到该队列。
2、Fanout:忽略routingKey的值,交换机将消息发送到所有与自己绑定的队列(广播通信)。
3、Topic:使用routingKey进行通配符匹配,类似模糊查询的匹配方式。
4、Headers:交换机不使用routingKey进行匹配,转而使用消息的头部消息进行匹配。
- 创建队列
await channel.QueueDeclareAsync(queue:"Q1",durable: false, exclusive: false, autoDelete: false);
声明队列方法有4个参数
1、queue:设置队列名称。
2、durable:RabbitMQ节点重启时队列是否保留。
3、exclusive:没有消费者连接时队列时删除队列。
4、auto-delete:没有消费者订阅队列时删除队列。
RabbitMQ参数文档:Queues | RabbitMQ
- 交换机与队列绑定
创建了交换机和队列后,我们需要告诉交换机将消息发送给哪个队列。交换机和队列间的关系称为绑定。
await channel.QueueBindAsync(queue: "Q1", exchange: "X",routingKey:"Q1_key");
- 生产者发送消息
await channel.BasicPublishAsync(exchange: "X",body:Encoding.UTF8.GetBytes("C1你好"), routingKey: "Q1_key");
发送消息的routingKey需要与交换机绑定队列的routingKey保持一致。
1、生产者端代码
namespace P
{
internal class P
{
static async Task Main(string[] args)
{
var factory = new ConnectionFactory { HostName = "localhost" };
var connection = await factory.CreateConnectionAsync();
var channel = await connection.CreateChannelAsync();
await channel.ExchangeDeclareAsync("X", ExchangeType.Direct);
await channel.QueueDeclareAsync(queue:"Q1",durable: false, exclusive: false, autoDelete: false);
await channel.QueueDeclareAsync(queue: "Q2", durable: false, exclusive: false, autoDelete: false);
await channel.QueueBindAsync(queue: "Q1", exchange: "X",routingKey:"Q1_key");
await channel.QueueBindAsync(queue: "Q2", exchange: "X", routingKey: "Q2_key");
string message1 = "C1你好";
string message2 = "C2你好";
await channel.BasicPublishAsync(exchange: "X",body:Encoding.UTF8.GetBytes(message1), routingKey: "Q1_key");
Console.WriteLine($"P发送消息:{message1}");
await channel.BasicPublishAsync(exchange: "X", body: Encoding.UTF8.GetBytes(message2), routingKey: "Q2_key");
Console.WriteLine($"P发送消息:{message2}");
Console.ReadLine();
}
}
}
2、消费者C1
namespace C1
{
internal class C1
{
static async Task Main(string[] args)
{
var factory = new ConnectionFactory { HostName = "localhost" };
var connection = await factory.CreateConnectionAsync();
var channel = await connection.CreateChannelAsync();
await channel.ExchangeDeclareAsync("X", ExchangeType.Direct);
await channel.QueueDeclareAsync(queue: "Q1", durable: false, exclusive: false, autoDelete: false);
await channel.QueueBindAsync(queue: "Q1", exchange: "X", routingKey: "Q1_key");
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"C1接收到信息:{message}");
};
await channel.BasicConsumeAsync(queue: "Q1", autoAck: true, consumer: consumer);
Console.ReadLine();
}
}
}
3、消费者C2
namespace C2
{
internal class C2
{
static async Task Main(string[] args)
{
var factory = new ConnectionFactory { HostName = "localhost" };
var connection = await factory.CreateConnectionAsync();
var channel = await connection.CreateChannelAsync();
await channel.ExchangeDeclareAsync("X", ExchangeType.Direct);
await channel.QueueDeclareAsync(queue: "Q2", durable: false, exclusive: false, autoDelete: false);
await channel.QueueBindAsync(queue: "Q2", exchange: "X", routingKey: "Q2_key");
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"C2接收到信息:{message}");
};
await channel.BasicConsumeAsync(queue: "Q2", autoAck: true, consumer: consumer);
Console.ReadLine();
}
}
}
补充:消费者端创建队列和交换机的步骤是可以省略,前提是保证生产者端的代码先执行。
4、输出结果
补充
RabbitMQ官方文档:RabbitMQ Documentation | RabbitMQhttps://www.rabbitmq.com/docs
版权归原作者 独步寻花_ 所有, 如有侵权,请联系我们删除。