消息丢失的情况在rabbitmq的消息的任何一个节点都会出现丢失。
生产者没有发送成功
因为网络等原因生产者在向MQ发送消息过程中MQ没有成功接收,但生产者却以为MQ成功接收了消息,不会再次重复发送该消息,从而导致消息的丢失。
解决方案:
1.事务机制
rabbitmq提供了事务功能,生产者发送数据之前开启rabbitmq的事务,然后发送消息,如果消息没有成功被rabbitmq接收,那生产者会收到异常报错,此时就可以回滚事务,然后尝试发送消息,如果rabbitmq接收了消息,那就可以提交事务。代码如下:
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
// RabbitMQ服务器连接信息
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
// 创建一个通道
$channel = $connection->channel();
// 定义队列名称
$queueName = 'transaction_queue';
// 开启事务
$channel->tx_select();
try {
// 生产者发布消息
for ($i = 0; $i < 5; $i++) {
$messageBody = "Message $i";
$message = new AMQPMessage($messageBody);
// 发布消息到队列
$channel->basic_publish($message, '', $queueName);
echo " [x] Sent '$messageBody'\n";
}
// 提交事务
$channel->tx_commit();
} catch (\Exception $e) {
// 发生异常时回滚事务
$channel->tx_rollback();
echo "Error: " . $e->getMessage() . "\n";
} finally {
// 关闭通道和连接
$channel->close();
$connection->close();
}
?>
在生产者示例中,我们使用 $channel->tx_select()
开启事务,然后在发布消息后使用 $channel->tx_commit()
提交事务。如果在事务提交期间发生异常,可以使用 $channel->tx_rollback()
进行回滚。
2.confirm机制(最常用的就是发布确认机制)
发布者确认是 在通道级别使用以下方法启用:confirm_select,
将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。
如果消息成功写入 RabbitMQ 中,RabbitMQ 会给生产者回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,生产者可以重新发送。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么可以重发。
发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息
PHP 生产者和消费者的示例,演示如何使用 RabbitMQ 的确认模式,批量发布消息,异步处理发布者确认、丢失消息回调,以及手动 ACK 模式。在这个示例中,消息和队列都被持久化。
生产者 Demo
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
// RabbitMQ 服务器连接信息
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
// 创建一个通道
$channel = $connection->channel();
// 定义队列名称
$queueName = 'confirm_mode_queue';
// 声明队列为持久化
$channel->queue_declare($queueName, false, true, false, false);
// 设置消息为持久化
$deliveryMode = AMQPMessage::DELIVERY_MODE_PERSISTENT;
try {
// 开启确认模式(Publisher Confirms)
$channel->confirm_select();
// 生产者批量发布消息
for ($i = 0; $i < 5; $i++) {
$messageBody = "Message $i";
$message = new AMQPMessage($messageBody, ['delivery_mode' => $deliveryMode]);
// 发布消息到队列
$channel->batch_basic_publish($message, '', $queueName);
echo " [x] Sent '$messageBody'\n";
}
// 设置异步发布者确认的回调函数
$channel->set_ack_handler(function (AMQPMessage $message) {
echo " [x] Message confirmed: ", $message->getBody(), "\n";
});
// 设置异步丢失消息的回调函数
$channel->set_nack_handler(function (AMQPMessage $message) {
echo " [x] Message lost: ", $message->getBody(), "\n";
});
// 提交批量发布
$channel->publish_batch();
// 等待确认,确保消息被正确发送
if ($channel->wait_for_pending_acks()) {
echo " [x] All messages sent successfully.\n";
} else {
throw new Exception("Failed to confirm messages to RabbitMQ.");
}
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
} finally {
// 关闭通道和连接
$channel->close();
$connection->close();
}
?>
消费者 Demo
<?php
require_once __DIR__ . '/vendor/autoload.php';
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
// RabbitMQ 服务器连接信息
$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
// 创建一个通道
$channel = $connection->channel();
// 定义队列名称
$queueName = 'confirm_mode_queue';
// 声明队列为持久化
$channel->queue_declare($queueName, false, true, false, false);
echo " [*] Waiting for messages. To exit press Ctrl+C\n";
// 回调函数处理消息
$callback = function ($msg) {
echo ' [x] Received ', $msg->body, "\n";
// 模拟处理消息的操作
// ...
// 模拟消息处理成功和失败的情况
$success = mt_rand(0, 1);
if ($success) {
// 处理成功,手动 ACK
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
echo " [x] Acknowledged\n";
} else {
// 处理失败,拒绝消息并重新入队
$msg->delivery_info['channel']->basic_reject($msg->delivery_info['delivery_tag'], true);
echo " [x] Requeued\n";
}
};
// 设置预取计数
$channel->basic_qos(null, 1, null);
// 消费消息,手动 ACK
$channel->basic_consume($queueName, '', false, false, false, false, $callback);
// 循环等待消息
while ($channel->is_consuming()) {
$channel->wait();
}
// 关闭通道和连接
$channel->close();
$connection->close();
?>
我们使用了 batch_basic_publish
来批量发布消息,并设置了异步发布者确认和丢失消息的回调函数。在消费者示例中,我们模拟了消息处理成功和失败的情况,并分别手动 ACK 或拒绝消息重新入队。这样可以确保消息在处理成功的情况下被确认,而在处理失败的情况下被重新入队。
RabbitMQ接收到消息之后丢失
RabbitMQ接收到生产者发送过来的消息,是存在内存中的,此时如果消息没有被消费完,RabbitMQ宕机了,那么再次启动的时候,内存中的消息就丢失了。
解决方案:
开启RabbitMQ的持久化,
// 声明队列为持久化
$channel->queue_declare($queueName, false, true, false, false);
队列持久化,队列持久化后,rabbitmq重启后,队列依旧存在,但不会持久化队列里面的数据,因此我们还需要把数据持久化。
$deliveryMode = 2;
$message = new AMQPMessage($messageBody, ['delivery_mode' => $deliveryMode]);
消息持久化,发送消息的时候将消息的 delivery_mode设置为 2
注意:持久化要起作用必须同时设置这两个持久化才行。
保证消息不丢失,除了开启队列、消息持久化的同时,也要开启 confirm(发布确认) 模式。这是因为我们在生产者中设置了队列持久化、消息持久化,但依然存在消息被传送到队列上,还没来得及存储在磁盘上,队列就宕机了,这种情况下消息也是会丢失的。所以在之前两步的基础上还是进行第三步:发布确认。三步操作加一起才能保证消息 从生产者到RabbitMQ服务器这个过程 是不丢失的。
消费者弄丢了消息
RabbitMQ成功的把消息发送给了消费者,RabbitMQ的ack机制会自动的返回成功,表明发送消息成功,下次就不会发送这个消息。但此时,消费者还没处理完该消息,然后宕机了,那么这个消息就丢失了。
解决方案:
RabbitMQ消息应答机制分为两种:自动应答、手动应答。对于此类消息丢失,消费者必须使用手动应答,此时如果消息没有处理完成,则不对rabbitmq发送ack,此时rabbitmq则认为消息没处理完成,则把消息重新入队,如果此时有其他消费者,则将其发送其他消费者处理。