持久化(Durable)
为了数据处理的效率问题,大多数消息队列产品基本都是内存型的一种数据存储模式,包括 Redis、Es等,当我们的业务进行非常顺利并且快速的时候这种模式,一个发送一个立刻就马上消费掉了,在这种情况下就发挥出了内存的优点速度快,占用空间小,但是这里有一个问题,如果业务场景并没有像我们预料的一样,而是出现了生产速度非常快,但消费跟不上,就会产生消息堆积。更有特殊一点的情况,服务宕机重启以后,内存中的数据就会丢失。遇到这种情况,我们就需要考虑对消息进行持久化,就和 Redis的 AOF、RDB的概念是一样的,不过这里需要注意的是在 RabbitMQ中队列,消息、交换机(如我们在发布订阅模式中提到的 Fanout Exchange (扇出交换机)等)都需要分别指定持久化。
队列的持久化
这里我们首先要了解在队列定义的时候我们可以看到后面跟了几个参数,分别为:
- queue (默认为 ‘’ ) 队列名称 声明的队列的名称
- passive (默认为 false) 检查队列是否存在
- durable (默认为 false) 持久化 是否将队列持久化到磁盘
- exclusive (默认为 false) 是否独占队列 独占队列只能被声明者的连接使用,并且在连接断开时会自动删除
- auto_delete (默认为true)在所有消费者断开连接后是否自动删除队列
$channel->queue_declare('hello', false, false, false,false);
其中我们需要用到的就是第三个参数 durable ; 将这个参数设置为 true 设置完成以后,这个 hello 这个队列就会持久化保存在磁盘之中,这样重启以后 hello这个队列是不会消失的;
消息的持久化
在队列持久化以后还不行,比如我们向这个队列中 发送了好几个消息
// 创建一个持久化的队列
$channel->queue_declare('hello', false, true, false, false);
// 创建消息
$msg = new AMQPMessage('消息');
// 发送一个消息到队列 hello中
$channel->basic_publish($msg, '', 'hello');
这个时候如果 RabbitMQ进行重启以后,我们会发现队列 hello是存在的,但是里面的消息已经被清空了所以这个时候我们要设置消息的持久化
// 创建一个持久化的队列
$channel->queue_declare('hello', false, true, false, false);
// 创建消息
$msg = new AMQPMessage('Hello World!', ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);
// 发送一个消息到队列 hello中
$channel->basic_publish($msg, '', 'hello');
交换机的持久化
这里我们首先要了解在交换机定义的时候我们可以看到后面跟了几个参数,分别为:
- exchange 交换机的名称
- type 交换机类型
-
- direct: 直连交换机,将消息路由到与绑定键完全匹配的队列。
-
- fanout: 扇形交换机,将消息广播到所有与其绑定的队列。
-
- topic: 主题交换机,将消息根据通配符模式匹配绑定键路由到队列。
-
- headers: 头交换机,根据消息的头部属性进行匹配。
- passive (默认为 false) 检查队列是否存在
- durable (默认为 false) 持久化 是否将队列持久化到磁盘
- exclusive (默认为 false) 是否独占队列 独占队列只能被声明者的连接使用,并且在连接断开时会自动删除
- auto_delete (默认为true)在所有消费者断开连接后是否自动删除队列
// 定义交换机
$channel->exchange_declare('orders', 'fanout', false, false, false);
$data = '订单号:' . time();
$msg = new AMQPMessage($data, ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);
惰性队列(Lazy Queues)
惰性队列是 RabbitMQ 3.6 版本引入的特性,它专门用于处理大量未消费的消息。
-
特点:惰性队列会将消息存储在磁盘上而不是内存中,以减少内存的使用。这对于处理大量消息的队列非常有用,因为它可以显著减少内存使用量,并提高性能。
-
用法:在声明队列时,可以通过在参数中设置 x-queue-mode 为 lazy 来创建惰性队列。例如:
$channel->queue_declare('hello', false, true, false, false, false, [
'x-queue-mode' => 'lazy'
]);
这样声明的队列会将消息存储在磁盘上,而不是常规的内存中,从而实现惰性加载。
持久化和惰性队列区别总结
- 持久化:关注于 RabbitMQ 服务器重启后,对象(交换机、队列、消息)的存活性和保持状态。
- 惰性队列:是一种优化策略,用于减少内存使用,特别是对于大量未消费的消息队列。
ACK确认
ACK 是指消费者告知 RabbitMQ 一个特定消息已经被接收和处理完毕的机制。这种机制是保证消息传递的可靠性和完整性的重要部分。
比如消费者处理失败了。这时,这条消息其实是没有被正确处理的。但是它又已经从消息队列中被删除移走了,这就产生了消息的丢失。ACK 机制,实际上就是说,当消费者出现问题,或者消费者的连接中断后,这条消息如果没有被确认消费,那么它就会重新加回到原来的消费队列中再次被消费。
手动确认
手动确认是指消费者在处理完消息后,显式地发送一个确认(ACK)给 RabbitMQ,告知消息已被正确处理。
这里需要再消费消息的时候修改一下参数
- queue 队列的名称
- consumer_tag 消费者标签,用于唯一标识连接中的消费者。如果未指定,RabbitMQ 会自动生成一个消费者标签。
- no_local 如果设置为 true,表示不接收此连接发布的消息。一般情况下设为 false,即接受本地(当前连接)发布的消息。
- no_ack 如果设置为 true,表示使用自动确认模式。在自动确认模式下,消费者收到消息后会自动向 RabbitMQ 发送确认(ACK),无需手动确认。
- exclusive 如果设置为 true,表示此消费者独占该队列。其他连接不能访问此队列。通常用于临时队列的创建。
- callback 回调函数
// 修改前
$channel->basic_consume('hello', '', false, true, false, false, $callback);
// 修改后
$channel->basic_consume('hello', '', false, false, false, false, $callback);
不管是客户端连接失败、报异常、还是超过指定的 rabbit.conf 文件中设置的超时时间,这条消息都会被重新放回到原来的队列中。
超时时间默认是 30 分钟,在 rabbit.conf 文件中通过 consumer_timeout 进行配置。
// 定义接收数据的回调函数
$callback = function ($msg) {
echo '接收到数据: ', $msg->body, PHP_EOL;
// 手动确认
$msg->ack();
};
// 消费队列,获取到数据将调用 callback 回调函数
$channel->basic_consume('hello', '', false, true, false, false, $callback);
自动确认
no_ack 如果设置为 true,表示使用自动确认模式。在自动确认模式下,消费者收到消息后会自动向 RabbitMQ 发送确认(ACK),无需手动确认。
自动确认和自动确认的区别和选择
手动确认更加可靠和精确,因为你可以在确保消息已经被完全处理后再发送确认,避免消息丢失或者重复处理。
自动确认简化了消费者的代码,但在处理消息失败时可能会导致消息丢失或者重复处理的问题。因此,对于一些关键性很高的应用,推荐使用手动确认以确保消息处理的可靠性。
发布确认
除了消息的确认之外,还有发布确认。上面的 ACK 确认,确认的是消息是否被消费完成。而发布确认,则是说消息是否被发布到了队列中。这个概念的关键点在于 RabbitMQ 中,有交换机,有队列两层处理。我们要确保消息发送到了队列中,然后在队列中,有相应的持久化机制就可以保证消息不丢。
// 连接 RabbitMQ 服务器
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();
// 将频道设置为确认模式
$channel->confirm_select();
// 定义发布消息的回调函数
$callback = function ($message) {
echo "发布确认成功!Delivery Tag: {$message->delivery_info['delivery_tag']}\n";
};
// 设置发布确认回调
$channel->set_ack_handler($callback);
$channel->set_nack_handler(function ($message) {
echo "发布确认失败!Delivery Tag: {$message->delivery_info['delivery_tag']}\n";
});
// 发布消息
$channel->basic_publish(new AMQPMessage('Hello RabbitMQ!'), '', 'hello_queue');
// 等待所有发布确认完成,超时时间为 5 秒。
$channel->wait_for_pending_acks(5);
// 关闭频道和连接
$channel->close();
$connection->close();
Redis中的ACK机制
Redis 中的 List ,还有 PubSub 以及 Stream 这些功能,并不算是一个完备的消息队列应用。最主要的原因,就是 Redis 中没有 ACK 机制。
但是我们可以通过一些业务逻辑来尽可能的弥补和补充比如 Laravel 就有一个重试的功能。它可能不是完全的 ACK 机制,但也可以视为是 ACK 机制的一个补充。我们可以在运行 Job 时指定重试次数。
php artisan queue:work --tries=3
这样,队列中的数据就有三次被重试执行的机会。我们可以在 Job 中直接抛出异常,模拟消费失败。