php rabbitmq生产者代码

<?php

namespace services\common;

use common\components\Service;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Exchange\AMQPExchangeType;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Wire\AMQPTable;
use Exception;
use Closure;
use common\enums\RabbitMqEnum;
use Yii;
use yii\redis\Connection;

/**
 * RabbitMq服务类
 * Date: 2021/8/20
 * Time: 14:56
 */
class RabbitMqService extends Service
{


    public $host;
    public $port = 5672;
    public $userName;
    public $passWord;
    public $setting = [
        'vhost' => '/',
        'insist' => false,
        'login_method' => 'AMQPLAIN',
        'login_response' => null,
        'locale' => 'en_US',
        'connection_timeout' => 3.0,
        'read_write_timeout' => 3.0,
        'context' => null,
        'keepalive' => false,
        'heartbeat' => 0
    ];

    // Send a message with the string "quit" to cancel the consumer.
    public $cancelTag = 'quit';

    public $connection = null;





    /**
     * 获取一个rabbitMQ连接
     * @return AMQPStreamConnection
     * @throws Exception
     */
    public function connect()
    {
        $rabbitMqConfig = \Yii::$app->getComponents()['rabbitMq'];
        unset($rabbitMqConfig['class']);
        if ($rabbitMqConfig) {
            $this->host = $rabbitMqConfig['host'];
            $this->port = $rabbitMqConfig['port'];
            $this->userName = $rabbitMqConfig['userName'];
            $this->passWord = $rabbitMqConfig['passWord'];
            $this->setting = $rabbitMqConfig['setting'];
        }

        $config = [
            'host' => $this->host,
            'port' => $this->port,
            'user' => $this->userName,
            'password' => $this->passWord,
            'vhost' => $this->setting['vhost'],
            'insist' => $this->setting['insist'],
            'login_method' => $this->setting['login_method'],
            'login_response' => $this->setting['login_response'],
            'locale' => $this->setting['locale'],
            'connection_timeout' => $this->setting['connection_timeout'],
            'read_write_timeout' => $this->setting['read_write_timeout'],
            'context' => $this->setting['context'],
            'keepalive' => $this->setting['keepalive'],
            'heartbeat'=>$this->setting['heartbeat']
        ];

        $connection = new AMQPStreamConnection(...array_values($config));

        if (!$connection->isConnected()) throw new Exception('Connect failed!');
        $this->connection = $connection;

        return $connection;
    }


    /**
     * 生产一个消息并发送到指定(direct)交换机
     * @param string $exchange
     * @param string $queue_name  队列名称
     * @param array $data
     * @param string $handler_class
     * @param string $method
     * @param Closure $error_callback
     * @param Closure $success_callback
     * @param string $exchange_type   交换机类型:默认直连交换机
     * @throws Exception
     * @return int
     */
    public function push(string $exchange, string $queue_name, array $data, string $handler_class = '', string $method = '', Closure $error_callback = null, Closure $success_callback = null,$exchange_type = AMQPExchangeType::DIRECT)
    {
        $message['data'] = $data;
        $message['handler_class'] = $handler_class;
        $message['method'] = $method;

        /** @var AMQPStreamConnection $conn */
        $conn = $this->connect();
        $channel = $conn->channel();
        $channel->exchange_declare($exchange, $exchange_type, false, false, false);
        $channel->queue_declare($queue_name, false, true, false, false);
        //绑定队列到交换机
        $channel->queue_bind($queue_name, $exchange, $queue_name);
        $message_id = $this->createMessageId();


        $body = new AMQPMessage(json_encode($message,JSON_UNESCAPED_UNICODE), array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, 'message_id' => $message_id, 'application_headers' => new AMQPTable(['retry' => 0])));

        //消息发送状态回调
        $channel->set_ack_handler(function (AMQPMessage $message) use ($success_callback) {
            !empty($success_callback) && $success_callback($message);
        });

        $channel->set_nack_handler(function (AMQPMessage $message) use ($error_callback) {
            !empty($error_callback) && $error_callback($message);
        });
        //开启消息发送状态监听
        $channel->confirm_select();

        $res = $this->setMessage($message_id, json_encode($message));
        if (!$res) {
            throw new Exception('设置消息失败');
        }

        $channel->basic_publish($body, $exchange, $queue_name);
        $channel->wait_for_pending_acks();

        $channel->close();
        $conn->close();

        return $message_id;
    }

    /**
     * 延时队列生产
     * @notice:此延时队列不能保证时间的精准,当业务处理出现阻塞,则在队列里已达到过期时间的消息并不会被发送到对应的队列。
     * @param int $sec 延时秒数
     * @param array $data 传递的数据
     * @param string $handler_class 处理类
     * @param string $method 处理方法
     * @param array $params 处理类参数
     * @throws Exception
     * @return void
     */
    public function delay(int $sec, array $data, string $handler_class = '', string $method = '', array $params = [])
    {
        $message['data'] = $data;
        $message['handler_class'] = $handler_class;
        $message['method'] = $method;
        $message['params'] = $params;
        $micro_sec = $sec * 1000;
        /** @var AMQPStreamConnection $conn */
        $conn = $this->connect();
        $channel = $conn->channel();
        $channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY, 'direct', false, false, false);
        $channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY_CACHE, 'direct', false, false, false);

        //死信交换机和路由
        $tale = new AMQPTable();
        $tale->set('x-dead-letter-exchange', RabbitMqEnum::EXCHANGE_DELAY);
        $tale->set('x-dead-letter-routing-key', RabbitMqEnum::DELAY_HANDLE_QUEUE);
        $tale->set('x-message-ttl', $micro_sec);

        $queue_name = 'delay_cache_queue_' . $sec . 's';
        //延时缓存队列声明及绑定
        $channel->queue_declare($queue_name, false, true, false, true, false, $tale);
        $channel->queue_bind($queue_name, RabbitMqEnum::EXCHANGE_DELAY_CACHE, $queue_name);

        //延时处理队列声明及绑定
        $channel->queue_declare(RabbitMqEnum::DELAY_HANDLE_QUEUE, false, true, false, false, false);
        $channel->queue_bind(RabbitMqEnum::DELAY_HANDLE_QUEUE, RabbitMqEnum::EXCHANGE_DELAY, RabbitMqEnum::DELAY_HANDLE_QUEUE);
        $message_id = $this->createMessageId();
        $res = $this->setMessage($message_id, json_encode($message),$sec+3600);
        if (!$res) {
            throw new Exception('设置消息失败');
        }
        $body = new AMQPMessage(json_encode($message), array(
            'expiration' => $micro_sec,
            'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
            'message_id' => $message_id
        ));

        $channel->basic_publish($body, RabbitMqEnum::EXCHANGE_DELAY_CACHE, $queue_name);
        $channel->close();
        $conn->close();
    }

    /**
     * 延时队列生产
     * @notice:此延时队列不能保证时间的精准,当业务处理出现阻塞,则在队列里已达到过期时间的消息并不会被发送到对应的队列。
     * @param int $sec 延时秒数
     * @param array $data 传递的数据
     * @param string $handler_class 处理类
     * @param string $method 处理方法
     * @param array $params 处理类参数
     * @throws Exception
     * @return void
     */
    public function delayTask(int $sec, array $data, string $handler_class = '', string $method = '', $consume_type_key = 'delay', array $params = [])
    {
        $message['data'] = $data;
        $message['handler_class'] = $handler_class;
        $message['method'] = $method;
        $message['params'] = $params;
        $micro_sec = $sec * 1000;
        $consume_type = RabbitMqEnum::getConsumeType($consume_type_key);
        /** @var AMQPStreamConnection $conn */
        $conn = $this->connect();
        $channel = $conn->channel();
        $channel->exchange_declare($consume_type['exchange'], 'direct', false, false, false);
        $channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY_CACHE, 'direct', false, false, false);

        //死信交换机和路由
        $tale = new AMQPTable();
        $tale->set('x-dead-letter-exchange', $consume_type['exchange']);
        $tale->set('x-dead-letter-routing-key', $consume_type['queue_name']);
        $tale->set('x-message-ttl', $micro_sec);

        $queue_name = $consume_type_key == 'delay' ? 'delay_cache_queue_' . $sec . 's' : 'delay_'.$consume_type_key.'_' . $sec . 's';
        //延时缓存队列声明及绑定
        $channel->queue_declare($queue_name, false, true, false, true, false, $tale);
        $channel->queue_bind($queue_name, RabbitMqEnum::EXCHANGE_DELAY_CACHE, $queue_name);

        //延时处理队列声明及绑定
        $channel->queue_declare($consume_type['queue_name'], false, true, false, false, false);
        $channel->queue_bind($consume_type['queue_name'], $consume_type['exchange'], $consume_type['queue_name']);
        $message_id = $this->createMessageId();
        $res = $this->setMessage($message_id, json_encode($message),$sec+3600);
        if (!$res) {
            throw new Exception('设置消息失败');
        }
        $body = new AMQPMessage(json_encode($message), array(
            'expiration' => $micro_sec,
            'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
            'message_id' => $message_id
        ));

        $channel->basic_publish($body, RabbitMqEnum::EXCHANGE_DELAY_CACHE, $queue_name);
        $channel->close();
        $conn->close();
    }

    /**
     * 消费者监听
     * @param string  $exchange
     * @param string  $queue_name
     * @param Closure $callback
     * @param string $exchange_type   交换机类型:默认直连交换机
     * @throws Exception
     * @return void
     */
    public function listen(string $exchange, string $queue_name, Closure $callback = null,$exchange_type = AMQPExchangeType::DIRECT)
    {
        /** @var AMQPStreamConnection $conn */
        $conn = $this->connect();
        if (!$conn) {
            throw new Exception('Connect failed!');
        }
        $channel = $conn->channel();
        $channel->exchange_declare($exchange, $exchange_type, false, false, false);
        $channel->queue_declare($queue_name, false, true, false, false);
        //绑定队列到交换机
        $channel->queue_bind($queue_name, $exchange, $queue_name);
        $channel->basic_qos(null, 1, null);

        $channel->basic_consume($queue_name, '', false, false, false, false, function (AMQPMessage $message) use ($callback, $channel, $exchange, $queue_name) : void {
            //消息确认
            $message->getChannel()->basic_ack($message->getDeliveryTag());

            //消息判断是否消费过
            $message_id = $message->get('message_id');
            //echo 'message_id:' . $message_id.PHP_EOL;


            $message_content = $this->checkMessage($message_id);

            if (!empty($message_content) && !empty($callback)) {
                $res = $callback($message);

                if (false === $res) {
                    //投递到 重试队列
                    $this->retry($message);

                } else {
                    //消费成功,删除消息
                    $this->deleteMessage($message_id);
                }
            }
            //!empty($message_content) && !empty($callback) && $callback($message);


        });
        while (count($channel->callbacks)) {
            $channel->wait();
        }
        $channel->close();
        $conn->close();
    }

    /**
     * 取出一个队列里的消息
     * @param string $queue_name
     * @return AMQPMessage|null
     * @throws
     */
    public function pull(string $queue_name)
    {
        /** @var AMQPStreamConnection $conn */
        $conn = $this->connect();
        $channel = $conn->channel();
        $channel->queue_declare($queue_name, false, true, false, false);
        /* @var AMQPMessage $message */
        $message = $channel->basic_get($queue_name, true);
        return $message;
    }


    /**
     * 消息添加延时重试
     * @desc 消息消费失败,加入重试队列
     * @param object  $message
     * @param string $exchange_type   交换机类型:默认直连交换机
     * @throws Exception
     * @return void
     */
    public function retry($message,$exchange_type = AMQPExchangeType::DIRECT)
    {
        $message_id = $message->get('message_id');

        //headersObject 是一个AMQPTable对象
        $headersObject = $message->get_properties()['application_headers'];
        $headersArr = $headersObject->getNativeData();

        //投递到重试队列
        $headersArr['retry'] = intval($headersArr['retry']);
        $headersArr['retry']++;

        $sec = $headersArr['retry'] * 20;  //N个20秒后执行
        $micro_sec = $sec * 1000;

        if ($headersArr['retry'] <= RabbitMqEnum::MAX_RETRY_NUM) {
            //这里加入 延时重试队列
            $conn = $this->connect();
            $channel = $conn->channel();
            $channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY_RETRY, $exchange_type, false, false, false);
            $channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY_RETRY_CACHE, $exchange_type, false, false, false);

            //死信交换机和路由
            $tale = new AMQPTable();
            $tale->set('x-dead-letter-exchange', RabbitMqEnum::EXCHANGE_DELAY_RETRY);
            $tale->set('x-dead-letter-routing-key', RabbitMqEnum::DELAY_RETRY_QUEUE);
            //$tale->set('x-message-ttl', $micro_sec);

            $queue_name = RabbitMqEnum::DELAY_RETRY_QUEUE . '_' . $sec . 's';
            //延时缓存队列声明及绑定
            $channel->queue_declare($queue_name, false, true, false, true, false, $tale);
            $channel->queue_bind($queue_name, RabbitMqEnum::EXCHANGE_DELAY_RETRY_CACHE, $queue_name);

            //延时处理队列声明及绑定
            $channel->queue_declare(RabbitMqEnum::DELAY_RETRY_QUEUE, false, true, false, false, false);
            $channel->queue_bind(RabbitMqEnum::DELAY_RETRY_QUEUE, RabbitMqEnum::EXCHANGE_DELAY_RETRY, RabbitMqEnum::DELAY_RETRY_QUEUE);


            $body = new AMQPMessage($message->getBody(), array(
                'expiration' => $micro_sec,
                'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
                'message_id' => $message_id,
                'application_headers' => new AMQPTable($headersArr)
            ));

            $channel->basic_publish($body, RabbitMqEnum::EXCHANGE_DELAY_RETRY_CACHE, $queue_name);

            $channel->close();
            $conn->close();

        } else {
            echo 'retry too many times' . PHP_EOL;

            //重试太多次
            Yii::error('message retry too many times , message_id :'.$message_id.' ,message content:' . $message->getBody());
            Yii::getLogger()->flush(true);
        }
    }


    /**
     * 生成MQ消息全局唯一ID
     * @return mixed
     */
    public function createMessageId()
    {
        $key = RabbitMqEnum::RABBITMQ_QUEUE_ID;
        $config = \Yii::$app->getComponents()['redis'];
        unset($config['class']);
        $redis = new Connection($config);
        $id = $redis->incr($key);
        $redis->close();
        return $id;
    }

    /**
     * redis设置消息信息
     * @param $message_id
     * @param $data
     * @return bool
     * @throws
     */
    public function setMessage($message_id, $data,$expire_time=86400)
    {
        $key = RabbitMqEnum::QUEUE_MESSAGE_KEY_PREFIX . $message_id;
        $config = \Yii::$app->getComponents()['redis'];
        unset($config['class']);
        $redis = new Connection($config);
        $res =  $redis->executeCommand('SET', [$key, json_encode($data), 'EX', $expire_time, 'NX']);
        $redis->close();
        return $res;
    }

    /**
     * 判断消息是否存在(不存在:已消费)
     * @param $message_id
     * @return mixed
     * @throws
     */
    public function checkMessage($message_id)
    {
        $key = RabbitMqEnum::QUEUE_MESSAGE_KEY_PREFIX . $message_id;
        $config = \Yii::$app->getComponents()['redis'];
        unset($config['class']);
        $redis = new Connection($config);

        $res = $redis->executeCommand('GET', [$key]);
        $redis->close();
        return $res;
    }

    /**
     * 消息消费完,删除
     * @param $message_id
     * @return mixed
     */
    public function deleteMessage($message_id)
    {
        $key = RabbitMqEnum::QUEUE_MESSAGE_KEY_PREFIX . $message_id;
        $config = \Yii::$app->getComponents()['redis'];
        unset($config['class']);
        $redis = new Connection($config);
        $res = $redis->del($key);
        $redis->close();
        return $res;
    }


}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值