| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- <?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连接
- * @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;
- }
- }
|