RabbitMq之消费者消息确认机制

860 阅读4分钟

最近项目里面使用到了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【待应答(待确认)的消息总数】

参考文章:

RabbitMQ PHP版

RabbitMQ消息确认机制(ACK)

RabbitMQ使用详解

RabbitMQ超详细学习笔记