RabbitMQ入门
本片文章会介绍以下章节,使大家能够有一些基本的认知。
- 什么是交换器、路由器、绑定,以及三者的关系
- 交换器类型
- 消费模式
- 交换器、队列、消息的持久化
- 消息发送的过程以及可靠消息投递机制
- 消息与队列的过期事件
- 死信队列
什么是交换器、路由器、绑定
RabbitMQ的很多强大功能和灵活性来自于AMQP规范。不像HTTP和SMTP协议,AMQP规范不仅定义了一种网络协议,同时也定义了服务器端的服务和行为。这些信息就是高级消息队列(Advanced Message Queuing,AMQ)模型。针对代理服务器软件,AMQ模型在逻辑上定义了三种抽象组件用于指定消息的路由行为:
■ 交换器(Exchange),消息代理服务器中用于把消息路由到队列的组件。
■ 队列(Queue),用来存储消息的数据结构,位于硬盘或内存中。
■ 绑定(Binding),一套规则,用于告诉交换器消息应该被存储到哪个队列。
交换器、路由键、绑定
交换器:Exchange
生产者将消息,发送到对应的 Exchange,由Exchange将消息路由到一个或者多个队列中去。如果路由不到,或许返回给生产者或许直接丢弃。
路由键:RoutingKey
生产者发送消息时,一般会指定RoutingKey,用于指定路由规则,RoutingKey需要与交换器类型和绑定键联合使用才能生效。
绑定:Binding
通过绑定把交换器与队列关联起来,在绑定的时候一般会指定一个bindkey,这样mq就知道如何正确的将消息路由到队列了。可以把RoutingKey看做是RoutingKey
交换器类型
Fanout
把发送到此交换机的消息,路由到所有绑定的队列。
Direct
把发送到此交换机的消息,路由到与BindingKey和RoutingKey完全匹配的队列中。
如图,如果发送 RoutingKey为warning的消息,会路由到 Queue1、Queue2中,如果key为 info、debug会路由到Queue2中,如果为其他的key,则不会路由到这两个队列中。
Topic
Direct是完全匹配,而Topic在匹配规则上进行了扩展,与Direct相似,也是将消息路由到BindKey和RoutingKey相匹配的队列中,但是匹配规则不同。
以 点号 “.” 作为分隔符,如 com.rabbitmq.client, com.hidden.client,java.util,concurrent,com.hidden.demo。
BindKey,RoutingKey也是通过 “.”分隔的字符串。
BindKey中可以存在两个字符 “*”,“#”,其中,# 标识 零至多个单词,* 标识 匹配一个单词
假如我们声明了如下RoutingKey
*.rabbitmq.*
*.*.client
com.#
匹配规则请看下图
Headers
将消息中的headers与该Exchange相关联的所有Binging中的参数进行匹配,如果匹配上了,则发送到该Binding对应的Queue中。
消费模式
在Rabbitmq中,有两种消费模式 :PULL(basic_get),PUSH(basic_consume),两者的区别是
get方法,每次获取一条,每次都是一条新的请求;而consume将channel(信道)设置为订阅模式,服务器会一直推送消息到消费者直到队列被消费完。
消费确认与拒绝
为了保证消息正确的到达消费者。Rabbitmq提供了消息确认机制。
当消费者在订阅队列时,可以指定autoAck参数,等于false时,rabbitmq会显示的等待消费者回复确认信号后,才从内存或硬盘中删除消息,当等于true是,会把发送的消息直接删除,而不管是否真的能到达消费者。
rabbitmq 不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递的唯一依据是消费者已经断开。
确认消息
$channel->basic_ack($deliveryTag,$requeue);
拒绝消息
$channel->basic_reject();
批量拒绝消息
$channel->basic_nack($deliveryTag,$multiple,$requeue);
操作与实践(PHP版本)
添加composer
"php-amqplib/php-amqplib": ">=2.9.0”
生产者
// 链接
$connection = new AMQPStreamConnection('localhost', '5672', 'guest', 'guest', '/');
$channel = $connection->channel();
// 声明 交换机 指定类型
$channel->exchange_declare('exchange-direct','direct’);
// 声明队列
$channel->queue_declare('queue-direct');
$channel->queue_declare('queue-direct1');
// 绑定队列-交换机-key,若一个队列里有多个routingkey,只能在回调里过滤
$channel->queue_bind('queue-direct','exchange-direct','a’);
$channel->queue_bind('queue-direct','exchange-direct’,'b’);
$channel->queue_bind('queue-direct1','exchange-direct’,'c’);
// 发送消息
$msg = new \PhpAmqpLib\Message\AMQPMessage('hello');
// 消息会到queue-direct1队列
$channel->basic_publish($msg, ‘exchange-direct’,’c');
消费者
include_once '../vendor/autoload.php';
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
$callback = function ($msg) {
echo 'routing_key', $msg->delivery_info['routing_key’],"\n";
echo ' [x] Received ', $msg->body, "\n";
};
$channel->basic_consume('queue-direct1', '', false, false, false, false, $callback);
// 采用 推模式
while ($channel->is_consuming()) {
$channel->wait();
}
持久化
持久化可以提高rabbitmq的可靠性,防止异常情况下(重启、关闭、宕机)的数据丢失。
rabbitmq的持久化分为3部分:交换机、队列、消息。
交换机、队列的持久化通过在声明时把durable设置为true来实现,但只能保证交换机、队列的元数据不会丢失,不能保证队列消息不回丢失。
消息的持久化需要设置 deliveryMode = 2,所以在使用持久化的时候,不能单单对某一项设置持久化。
$msg = new AMQPMessage('这是exchange的消息',['deliveryMode'=>2]);
需要注意的是,并不是设置为持久化,就100%确保数据不会丢失。
比如在autoAck时,消费者收到消息,还没有来得及处理发生宕机这样也算消息丢失,可以采用手动ack解决。
其次,持久化是需要写入硬盘的,但不是为每一条都进行同步存盘(参考fsync)操作,可能暂时保存在了缓冲区中,如果这个时间段发生异常,也会消息丢失。可以使用 镜像队列解决。
还可以使用 事务机制和确认机制来保证消息已经正确发送且存储。
消息发送的过程以及可靠消息投递机制
确认机制有两种方式
1. 事务确认机制
2. 发送方确认机制
事务确认机制
- channel.tx_select() 声明事务
- channel.tx_commit() 提交事务
- channel.tx_rollback() 回滚事务
可以看下图,AMQP的协议流转
发送方确认机制
RABBITMQ 可能会遇到一个问题,发送方并不知道消息是否真正到达MQ,在除了事务机制之外,引入了confirm模式,比事务机制更轻量。
生产者将信道设置成 confirm(确认)模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一 ID),这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。RabbitMQ 回传给生产者的确认消息中的 deliveryTag 包含了确认消息的序号,此外 RabbitMQ 也可以设置 channel.basicAck 方法中的 multiple 参数,表示到这个序号之前的所有消息都已经得到了处理。
事务机制在一条消息发送之后会使发送端阻塞,以等待 RabbitMQ 的回应,之后才能继续发送下一条消息。相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用程序便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该 nack 命令。
- channel.confirm() 将 信道 设置为confirm模式
示例:
// 生产者
// 链接
$connection = new AMQPStreamConnection('localhost', '5672', 'guest', 'guest', '/');
$channel = $connection->channel();
// 声明 交换机 指定类型
$channel->exchange_declare('exchange-direct','direct’);
// 声明队列
$channel->queue_declare('queue-direct');
// 绑定队列-交换机-key,若一个队列里有多个routingkey,只能在回调里过滤
$channel->queue_bind('queue-direct','exchange-direct','a’);
// 开启confirm模式
$channel->confirm_select();
// 设置nack 回调
$channel->set_nack_handler(function ($msg) {
var_dump($msg->body);
echo 'nack';
});
// 设置ack 回调
$channel->set_ack_handler(function ($msg) {
var_dump($msg->body);
echo 'ack';
});
// 发送消息
$msg = new \PhpAmqpLib\Message\AMQPMessage('hello’);
// 等待
$channel->wait_for_pending_acks_returns();
// 消息会到queue-direct队列
$channel->basic_publish($msg, ‘exchange-direct’,’a');
消息与队列的过期时间
1.设置消息的TTL
目前有两种方法设置,第一种是设置在队列上即队列上所有消息一起过期,第二种是 单独对消息进行设置过期时间,当两者一起使用时以 过期时间最小的为准,消息一旦过期就会变成 死信 (后边讲)。
第一种:在声明队列时,添加x-message-tt信息
$set = new AMQPTable();
$set->set('x-message-ttl', 3000);
$channel->queue_declare("queue-1", false, false, false, true, false, $set, null);
第二种:在发送消息时,添加expiration信息
$msg = new AMQPMessage('这是exchange-dlx的消息',['expiration'=>3000]);
2.设置队列的TTL
控制队列被自动删除前处于未使用状态的时间。
未使用的意思是:队列上没有任何消费者,队列没有重新声明,并且在过期时间段内没有被get过。
Rabbitmq会确保 队列在过期后删除队列,但不能保证很及时,另外Rabbitmq重启后,持久化队列的过期时间也会被重新计算。
$set = new AMQPTable();
$set->set('x-expire', 3000);
$channel->queue_declare("queue-1", false, false, false, true, false, $set, null);
死信
消息变成死信有以下几种情况:
1.消息被拒绝(basic.reject/basic.nack),且设置requeue为false。
2.消息过期
3.队列到达最大长度
基于消息的过期时间添加,x-dead-letter-exchange或x-dead-letter-routingkey。
需要注意,在设置参数时,交换机可以不存在,但是在死信到达的时候,交换机及队列一定要存在,不然会收不到消息。
$set = new AMQPTable();
$set->set('x-message-ttl', 3000);
$set->set('x-dead-letter-exchange', 'exchange');
$set->set('x-dead-letter-routingkey', ‘routingkey');
$channel->queue_declare("queue-1", false, false, false, true, false, $set, null);