封绝的世界

php rabbitmq的开发体验(三)

一、前言

在上一篇rabbitmq开发体验(二),我们正式的用我们php来操作消息队列的生产和消费,并利用的rabbitmq的高级特性来进行ack确认机制,幂等性,限流机制,重回机制,ttl,死信队列(相当于失败消息的回收站)。已经可以正常的使用,但消息消费异常问题罗列以下。

1、自动ack机制会导致消息丢失的问题;

简要代码如下,设置消息自动ack,这种情况下,MQ只要确认消息发送成功,无须等待应答就会丢弃消息,
这会导致客户端还未处理完时,出异常或断电了,导致消息丢失的后果。解决方法就是把代码里的no_ack参数值true,改成false,并在消息处理完后发ack响应。
注:自动ack还有个弊端,只要队列不空,RabbitMQ会源源不断的把消息推送给客户端,而不管客户端能否消费的完。

$this->channel->basic_consume(
    $this->query_name,
    '',     //customer_tag
    false,  //no_local
    true,  //no_ack 消息自动ack
    false,   //exclusive 排他消费者,即这个队列只能由一个消费者消费.适用于任务不允许进行并发处理的情况下.比如系统对接
    false,  //nowait

2、自动ack机制会导致消息丢失的问题;

为了解决问题1,做了改进,简要代码如下:

$this->channel->basic_consume(
    $this->query_name,
    '',     //customer_tag
    false,  //no_local
    false,  //no_ack 关闭自动ack,手工发送ack
    false,   //exclusive 排他消费者,即这个队列只能由一个消费者消费.适用于任务不允许进行并发处理的情况下.比如系统对接
    false,  //nowait

 

$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']); //手动在成功消费后发送ack

消费端完成后,再做ack响应,失败就不做ack响应,这样消息会储存在MQ的Unacked消息里,不会丢失,看起来没啥问题,
但是有一次,callback触发了一个bug,导致所有消息都抛出异常,然后队列的Unacked消息数暴涨,导致MQ响应越来越慢,甚至崩溃的问题。
原因是如果MQ没得到ack响应,这些消息会堆积在Unacked消息里,不会抛弃,直至客户端断开重连时,才变回ready;
如果Consumer客户端不断开连接,这些Unacked消息,永远不会变回ready状态,Unacked消息多了,占用内存越来越大,并且消费的很慢,都卡在unacked。

3、启用nack机制后,导致的死循环;

为了解决问题2,再调整一下代码,简要代码如下:

catch (Exception $e){
        $this->writeLog('runtime/vm_exception.log',$e->getMessage());
        //发送nack信息应答当前消息处理异常 第三个参数是否重回队列 默认false不重回队列
        $msg->delivery_info['channel']->basic_nack($msg->delivery_info['delivery_tag'],false,true);
}

嗯,改成这模样总没问题了吧,正常就ack,不正常就nack,并等下一次重新消费。
果然,又出问题了,这回又是callback出异常了,但是故障现象是Ready的消息猛增,一直不见减少。
原因是出异常后,把消息塞回队列头部,下一步又消费这条会出异常的消息,又出错,塞回队列……
进入了死循环了,当然新的消息不会消费,导致堆积了……

我的解决方案:

 

$retry = $this->getRetryCount($msg);

try {
        $routingKey = $this->getOrigRoutingKey($msg);
        $subMessage = new SubMessage($msg, $routingKey , [
              'retry_count' => $retry, // 重试次数
        ]);

        $this->subscribe($subMessage);

} catch (\Exception $ex) {
                
        $this->writeLog('runtime/vm_consume_failed.log', '消费失败!' . $ex->getMessage() . $msg->getBody());
        if ($retry > 3) {
               // 超过最大重试次数,消息无法处理
               $publishFailed($msg);
               return;
        }

        // 消息处理失败,稍后重试
        $publishRetry($msg);
}
    /**
     * 获取消息重试次数
     * @param AMQPMessage $msg
     * @return int
     */
    protected function getRetryCount($msg)
    {
        $retry = 0;
        if ($msg->has('application_headers')) {
            $headers = $msg->get('application_headers')->getNativeData();
            if (isset($headers['x-death'][0]['count'])) {
                $retry = $headers['x-death'][0]['count'];
            }
        }

        return (int)$retry;
    }
        // 发起延时重试
        $publishRetry = function ($msg) use ($queueName,$exchangeRetryName) {

            /** @var AMQPTable $headers */
            if ($msg->has('application_headers')) {
                $headers = $msg->get('application_headers');
            } else {
                $headers = new AMQPTable();
            }

            $headers->set('x-orig-routing-key', $this->getOrigRoutingKey($msg));

            $properties = $msg->get_properties();
            $properties['application_headers'] = $headers;
            $newMsg = new AMQPMessage($msg->getBody(), $properties);

            $this->channel->basic_publish(
                $newMsg,
                $exchangeRetryName,
                $queueName
            );
            //发送ack信息应答当前消息处理完成
            $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
        };
    /**
     * 声明重试队列
     */
    private function declareRetryQueue()
    {
        $this->channel->queue_declare($this->query_retry_name, false, true, false, false, false,new AMQPTable(array(
            'x-dead-letter-exchange' => $this->exchange_name,
            'x-dead-letter-routing-key' => $this->query_name,
            'x-message-ttl'          => 3 * 1000,
        )));
        $this->channel->queue_bind($this->query_retry_name, $this->exchange_retry_name, $this->query_name);
    }

 补充java有SpringRetry类处理RabbitMQ实现重试次数方法一-SpringRetry

4、队列优先级和消息优先级的介绍和使用

RabbitMQ在3.5.0版本的时候实现了优先级队列。任何一个队列都可以通过客户端配置参数方式设置一个优先级(但是不能使用策略的方式配置这个参数)。当前优先级的最大值为:255。这个值最好在1到10之间。

通过声明队列方式并使用参数x-max-priority指定当前的队列为优先级队列。这个优先级队列支持的参数必须是一个整数在1到255.

/**
     * 声明消费队列
     * @param $priority 消息队列优先级 暂时取个中间值 1-10
     */
    private function declareConsumeQueue($priority = 5)
    {
        //声明队列
        $this->channel->queue_declare(
            $this->query_name,  //队列名称
            false,     //passive消极的,如果该队列已存在,则不会创建;如果不存在,创建新的队列。
            true,      //durable持久的,指定队列持久
            false,   //exclusive独占的,是否能被其他队列访问true排他的。如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。
                                //排它是基于连接可见的,同一个连接不同信道是可以访问同一连接创建的排它队列,“首次”是指如果一个连接已经声明了一个排他队列,
                                //其他连接是不允许建立同名的排他队列,即使这个队列是持久化的,一旦连接关闭或者客户端退出,该排它队列会被自动删除,这种队列适用于一个客户端同时发送与接口消息的场景。
            false,  //设置是否自动删除。当所有消费者都与这个队列断开连接时,这个队列会自动删除。注意: 不是说该队列没有消费者连接时该队列就会自动删除,因为当生产者声明了该队列且没有消费者连接消费时,该队列是不会自动删除的。
            false,       //nowait
            new AMQPTable(array(
                'x-max-priority'         => $priority, //消息队列的优先级
            ))
        );
        //绑定交换机和队列 参数:队列名,交换机名,路由键名
        $this->channel->queue_bind($this->query_name, $this->exchange_name, $this->route_key_name);
        $this->channel->queue_bind($this->query_name, $this->exchange_name, $this->query_name);
    }

 

优先级队列总结如下:

1.优先级队列必须和优先级消息一起使用,才能发挥出效果,但是会消耗性能

2.优先级队列必须在消费者繁忙的时候,才能对消息按照优先级排序

3.非优先级队列发送优先级消息是不会排序的,所以向非优先级队列发送优先级消息是没有任何作用的

posted @ 2021-06-15 18:52  天边的云云  阅读(361)  评论(0编辑  收藏  举报