'/', '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连接 * @Author bing * @DateTime 2021-02-24 10:35:37 * @copyright: Copyright (c) 2020 广东七件事集团 * @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)交换机 * @Author bing * @DateTime 2021-02-24 15:28:47 * @copyright: Copyright (c) 2020 广东七件事集团 * @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(); Yii::$app->custom->rabbitMqLogs("rabbitmq-push 消息队列投递成功。投递ID:" . $message_id, json_encode($message)); return $message_id; } /** * 延时队列生产 * @notice:此延时队列不能保证时间的精准,当业务处理出现阻塞,则在队列里已达到过期时间的消息并不会被发送到对应的队列。 * @Author bing * @DateTime 2021-02-24 16:49:31 * @copyright: Copyright (c) 2020 广东七件事集团 * @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(); Yii::$app->custom->rabbitMqLogs("rabbitmq-delay 消息队列投递成功。投递ID:" . $message_id, json_encode($message)); } /** * 延时队列生产 * @notice:此延时队列不能保证时间的精准,当业务处理出现阻塞,则在队列里已达到过期时间的消息并不会被发送到对应的队列。 * @Author bing * @DateTime 2021-02-24 16:49:31 * @copyright: Copyright (c) 2020 广东七件事集团 * @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(); Yii::$app->custom->rabbitMqLogs("rabbitmq-delay-task 消息队列投递成功。投递ID:" . $message_id, json_encode($message)); } /** * 消费者监听 * @Author bing * @DateTime 2021-02-24 15:18:08 * @copyright: Copyright (c) 2020 广东七件事集团 * @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(); } /** * 取出一个队列里的消息 * @Author bing * @DateTime 2021-02-27 17:19:25 * @copyright: Copyright (c) 2020 广东七件事集团 * @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; } }