最近项目里面使用到了rabbitmq队列,遇到消费者消息确认的知识点,在此做个记录。
相关环境:PHP7.4 + RabbitMQ 3.3.5 + Laravel8
本文没有使用laravel框架的方法,使用PHP的mq组件。文章基于rabbitmq的工作模式来说明。
工作模式(work quene):当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。
什么是消息确认?
为了保证消息从队列可靠地到达消费者,RabbitMQ提供消息确认机制(Message Acknowledgment)。消费者在声明队列时,可以指定noAck参数,当noAck=false时,rabbitMQ会等待消费者显式发回ack信号后从内存(和磁盘,如果是持久化消息)中删除消息。这里需要注意。当 noAck 参数等于 true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了这些消息。如果一个消息设置了手动确认,就必须应答或者拒绝,否则会一直阻塞。
当noAck 参数为 false 时,对于 RabbitMQ 服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果 RabbitMQ 服务器端一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)。
RabbitMQ 中消息应答方式有两种:自动确认、手动确认。
手动确认主要使用一些方法:
- basicAck(long deliveryTag, boolean multiple):用于肯定确认,multiple参数用于确认多个消息。确认后从队列删除消息。
- basicRecover(bool) :消息重回队列。参数为true表示尽可能的将消息投递给其他消费者消费,而不是自己再次消费。false则表示在睡眠5s后消息重新投递给自己。
- basicReject(long deliveryTag, boolean requeue):接收端告诉服务器这个消息我拒绝接受,可以设置是否回到队列中还是丢弃。true则重新入队列,该消费者还是会消费到该条被reject的消息。false表示丢弃或者进入死信队列。
- basicNack(long deliveryTag, boolean multiple, boolean requeue):可以一次拒绝N条消息,客户端可以设置basicNack()的multiple参数为true。与basicReject()的区别就是同时支持多个消息,可以nack该消费者先前接收未ack的所有消息。nack后的消息也会被自己消费到。
我们直接看代码:
生产者代码:
public function sendBasicMsg() {
try {
$connection = new AMQPStreamConnection('127.0.0.1', 5672, 'guest', 'guest');
$channel = $connection->channel();
$queue = 'record-queue'; //队列名称
$channel->queue_declare($queue, false, false, false, false);
$data = [
'name' => 'YIF'
];
$msg = new AMQPMessage(json_encode($data));
$channel->basic_publish($msg, '', $queue);
echo " [x] Sent 'success'\n";
$channel->close();
$connection->close();
} catch (\Exception $e) {
Log::error("生产者发送消息异常:", [
'method' => __METHOD__,
'respone' => $e->getMessage() ?? ""
]);
}
}
消费者代码(自动确认不响应ack):
public function consume($consumeName)
{
try {
$connection = new AMQPStreamConnection('127.0.0.1', 5672, 'guest', 'guest');
$channel = $connection->channel();
$queue = 'record-queue'; //队列名称
$channel->queue_declare($queue, false, false, false, false);
echo " [*] Waiting for messages\n";
$callback = function ($msg) use ($consumeName) {
Log::info($consumeName . ' 消费啦===》' . $msg->body);
};
//第四个参数为false意味着要ack (true 意味着不响应ack)
$channel->basic_consume($queue, '', false, true, false, false, $callback);
while ($channel->is_open()) { //一直监听
$channel->wait();
}
$channel->close();
$connection->close();
} catch (\Exception $e) {
Log::error("消费者消费消息异常:", [
'method' => __METHOD__,
'respone' => $e->getMessage() ?? ""
]);
}
}
我们运行生产者代码,先产生消息到队列,生成5条消息,结果看图:
Ready:待消费的消息总数。
Unacked:待应答(待确认)的消息总数。
Total:总数 Ready+Unacked。
此时消费者还没有运行,所以Connections和Channels都没有数据。
我们接着运行消费者,消息为0,已经被消费了
上图第三个表格(最长那个)有一列是Ack required是白色圆圈,代表消费者没有开启ack。
我现在只运行一个消费者,所以Connections和Channels都只有1个数据,如果多个消费者则对应消费者数量的数据。
代码结果会打印log【下面结果是我开了2个消费者的情况,2个消费者消费很均匀,公平调度】:
[2022-10-29 07:21:38] local.INFO: consumeA 消费啦===》{"name":"YIF"}
[2022-10-29 07:21:40] local.INFO: consumeB 消费啦===》{"name":"YIF"}
[2022-10-29 07:21:43] local.INFO: consumeA 消费啦===》{"name":"YIF"}
[2022-10-29 07:21:44] local.INFO: consumeB 消费啦===》{"name":"YIF"}
消费者代码(手动确认响应ack):
public function consume($consumeName)
{
try {
$connection = new AMQPStreamConnection('127.0.0.1', 5672, 'guest', 'guest');
$channel = $connection->channel();
$queue = 'record-queue'; //队列名称
$channel->queue_declare($queue, false, false, false, false);
$callback = function ($msg) use ($consumeName) {
Log::info($consumeName . ' 消费啦===》' . $msg->body);
$msg->ack(); //发送ack
};
//第四个参数为false意味着要ack (true 意味着不响应ack)
$channel->basic_consume($queue, '', false, false, false, false, $callback);
while ($channel->is_open()) { //一直监听
$channel->wait();
}
$channel->close();
$connection->close();
} catch (\Exception $e) {
Log::error("消费者消费消息异常:", [
'method' => __METHOD__,
'respone' => $e->getMessage() ?? ""
]);
}
}
同样的我生产者先生成5条数据,这个和自动确认一样
开启两个消费者A与B
此时有2条连接对象(Connections和Channels也是各自两条数据,就不截图了),Ack required是黑色圆圈,代表消费者开启ack。
打印日志如下:
[2022-10-29 07:29:30] local.INFO: consumeA 消费啦===》{"name":"YIF"}
[2022-10-29 07:29:34] local.INFO: consumeB 消费啦===》{"name":"YIF"}
[2022-10-29 07:29:36] local.INFO: consumeA 消费啦===》{"name":"YIF"}
[2022-10-29 07:29:37] local.INFO: consumeB 消费啦===》{"name":"YIF"}
[2022-10-29 07:29:39] local.INFO: consumeA 消费啦===》{"name":"YIF"}
假如现在我把手动确认代码的$msg->ack();注释掉
public function consume($consumeName)
{
try {
$connection = new AMQPStreamConnection('127.0.0.1', 5672, 'guest', 'guest');
$channel = $connection->channel();
$queue = 'record-queue'; //队列名称
$channel->queue_declare($queue, false, false, false, false);
$callback = function ($msg) use ($consumeName) {
Log::info($consumeName . ' 消费啦===》' . $msg->body);
//$msg->ack(); //发送ack
};
//第四个参数为false意味着要ack (true 意味着不响应ack)
$channel->basic_consume($queue, '', false, false, false, false, $callback);
while ($channel->is_open()) { //一直监听
$channel->wait();
}
$channel->close();
$connection->close();
} catch (\Exception $e) {
Log::error("消费者消费消息异常:", [
'method' => __METHOD__,
'respone' => $e->getMessage() ?? ""
]);
}
}
此时代码是需要手动确认的【第四个参数为false】,但是没有发ack。
然后测一下,生产者生产一条数据,然后消费者A去消费,消费者消费成功的:
[2022-10-29 07:33:38] local.INFO: consumeA 消费啦===》{"name":"YIF"}
此时如图:
Unacked为1【待应答(待确认)的消息总数】
参考文章: