├── .gitignore ├── ClientInterface.php ├── LICENSE.md ├── Message.php ├── Queue.php ├── README.md ├── alimns ├── Client.php └── Queue.php ├── awsqs ├── Client.php └── Queue.php ├── composer.json └── redis ├── Client.php └── Queue.php /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | -------------------------------------------------------------------------------- /ClientInterface.php: -------------------------------------------------------------------------------- 1 | messageId; 45 | } 46 | 47 | /** 48 | * 获取消息详情 49 | */ 50 | public function getBody() 51 | { 52 | return $this->messageBody; 53 | } 54 | 55 | /** 56 | * 修改消息可见时间 57 | * @param int $delay 58 | */ 59 | public function release($delay = 60) 60 | { 61 | $this->queue->changeMessageVisibility($this->queue, $delay); 62 | } 63 | 64 | /** 65 | * 删除消息 66 | * @return bool 67 | */ 68 | public function delete() 69 | { 70 | return $this->queue->deleteMessage($this->receiptHandle); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Queue.php: -------------------------------------------------------------------------------- 1 | sendMessage($message, $delay)) { 38 | $successCount++; 39 | } 40 | } 41 | return $successCount; 42 | } 43 | 44 | /** 45 | * 消费消息 46 | * @return Message 47 | */ 48 | abstract public function receiveMessage(); 49 | 50 | /** 51 | * 批量消费消息 52 | * @param int $num 本次获取的消息数量 53 | * @return array 54 | */ 55 | public function batchReceiveMessage($num = 10) 56 | { 57 | $message = []; 58 | for ($i = 1; $i <= $num; $i++) { 59 | $message[] = $this->receiveMessage(); 60 | } 61 | return $message; 62 | } 63 | 64 | /** 65 | * 修改消息可见时间 66 | * @param string $receiptHandle 67 | * @param int $visibilityTimeout 68 | * @return bool 69 | */ 70 | abstract public function changeMessageVisibility($receiptHandle, $visibilityTimeout); 71 | 72 | /** 73 | * 删除消息 74 | * @param string $receiptHandle 75 | * @return bool 76 | */ 77 | abstract public function deleteMessage($receiptHandle); 78 | 79 | /** 80 | * 批量删除消息 81 | * @param array $receiptHandles 82 | * @return int 83 | */ 84 | public function batchDeleteMessage($receiptHandles) 85 | { 86 | $successCount = 0; 87 | foreach ($receiptHandles as $receiptHandle) { 88 | if ($this->deleteMessage($receiptHandle)) { 89 | $successCount++; 90 | } 91 | } 92 | return $successCount; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yii2-mq 2 | 3 | 适用于Yii2的消息服务组件,支持阿里云的MNS,AWS的SQS,以及Redis。 4 | 5 | [![Latest Stable Version](https://poser.pugx.org/xutl/yii2-mq/v/stable.png)](https://packagist.org/packages/xutl/yii2-mq) 6 | [![Total Downloads](https://poser.pugx.org/xutl/yii2-mq/downloads.png)](https://packagist.org/packages/xutl/yii2-mq) 7 | [![Reference Status](https://www.versioneye.com/php/xutl:yii2-mq/reference_badge.svg)](https://www.versioneye.com/php/xutl:yii2-mq/references) 8 | [![Build Status](https://img.shields.io/travis/xutl/yii2-mq.svg)](http://travis-ci.org/xutl/yii2-mq) 9 | [![Dependency Status](https://www.versioneye.com/php/xutl:yii2-mq/dev-master/badge.png)](https://www.versioneye.com/php/xutl:yii2-mq/dev-master) 10 | [![License](https://poser.pugx.org/xutl/yii2-mq/license.svg)](https://packagist.org/packages/xutl/yii2-mq) 11 | 12 | 13 | 14 | 特别说明:非任务队列,也不是短消息那种私信组件,这是个纯消息组件。本来我是想做队列,我看了laravel,以及yii2其他人做的队列任务组件,我发现, 15 | 他们下发任务的时候要么发个闭包,要么发个序列化的类,包括我之前做的一个队列组件也是这么做的,后来我看了阿里云的消息队列服务的开发者 16 | 文档我觉得,消息服务本质上就是个纯消息服务,没必要把任务也放里面,一条消息就是一个普通的JSON字符串就行了,就像微信的公众号接收 17 | 服务端消息一样,接到消息干什么,怎么干我觉得是客户端的事。 18 | 19 | 20 | ###队列说明 21 | 22 | 之前看yiisoft上那个队列半成品给我带到沟里了,且它自带的redis的一直有bug,常年不维护。 23 | 24 | 下面是队列说明: 25 | 26 | 1、插入队列的消息,可以是数组或者是json,不要直接把任务对象放入队列。 27 | 2、消费消息时,该消息只是进入了保留期,大概1分钟后又会重新进入队列。 28 | 3、如果你消费消息后,处理该消息失败,或者其他原因需要修改保留期有相应的方法修改。 29 | 4、在消息消费完,你需要手动删除该消息。 30 | 31 | 以上概念是按照 阿里云的 32 | https://help.aliyun.com/document_detail/27414.html 实现的 33 | 34 | 知乎上这篇甩锅我给我很大的启发。 35 | https://zhuanlan.zhihu.com/p/25192112 36 | 37 | ## 安装 38 | 39 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 40 | 41 | Either run 42 | 43 | ``` 44 | php composer.phar require --prefer-dist xutl/yii2-mq 45 | ``` 46 | 47 | or add 48 | 49 | ``` 50 | "xutl/yii2-mq": "~1.0.0" 51 | ``` 52 | 53 | to the require section of your `composer.json` file. 54 | 55 | ## 组件配置 56 | 57 | ````php 58 | //使用Redis 59 | 'queue' => [ 60 | 'class' => 'xutl\mq\redis\Client', 61 | 'redis' => [ 62 | 'scheme' => 'tcp', 63 | 'host' => '127.0.0.1', 64 | 'port' => 6379, 65 | //'password' => '1984111a', 66 | 'db' => 0 67 | ], 68 | ], 69 | 70 | //使用AWS SQS 71 | 'queue' => [ 72 | 'class' => 'xutl\mq\awsqs\Client', 73 | 'sqs' => [ 74 | //etc 75 | ], 76 | ], 77 | //使用阿里MNS 78 | 'queue' => [ 79 | 'class' => 'xutl\mq\alimns\Client', 80 | 'endPoint' => '', 81 | 'accessId'=>'', 82 | 'accessKey'=>'', 83 | ], 84 | ```` 85 | 86 | ## 使用 87 | 88 | ```php 89 | 90 | //入队 91 | /** @var \xutl\mq\Queue $queue */ 92 | $queue = Yii::$app->queue->getQueueRef('mailer'); 93 | for ($i = 0; $i < 500; $i++) { 94 | $queue->sendMessage([ 95 | 'aaa'=>'bbb', 96 | ]); 97 | } 98 | 99 | //出队 100 | /** @var \xutl\mq\Queue $queue */ 101 | $queue = Yii::$app->queue->getQueueRef('mailer'); 102 | /** @var \xutl\mq\Message $message */ 103 | while (($message = $queue->receiveMessage()) !== false) { 104 | $body = $message->getBody(); 105 | var_dump($body); 106 | $message->delete(); 107 | } 108 | ``` 109 | 110 | ## License 111 | 112 | This is released under the MIT License. See the bundled [LICENSE.md](LICENSE.md) 113 | for details. -------------------------------------------------------------------------------- /alimns/Client.php: -------------------------------------------------------------------------------- 1 | endPoint)) { 61 | throw new InvalidConfigException ('The "endPoint" property must be set.'); 62 | } 63 | if (empty ($this->accessId)) { 64 | throw new InvalidConfigException ('The "accessId" property must be set.'); 65 | } 66 | if (empty ($this->accessKey)) { 67 | throw new InvalidConfigException ('The "accessKey" property must be set.'); 68 | } 69 | $this->client = new HttpClient($this->endPoint, $this->accessId, $this->accessKey, $this->securityToken, $this->config); 70 | } 71 | 72 | /** 73 | * 获取队列 74 | * @param string $queueName 75 | * @return Queue 76 | */ 77 | public function getQueueRef($queueName) 78 | { 79 | return new Queue([ 80 | 'client' => $this->client, 81 | 'queueName' => $queueName, 82 | 'base64' => $this->base64 83 | ]); 84 | } 85 | } -------------------------------------------------------------------------------- /alimns/Queue.php: -------------------------------------------------------------------------------- 1 | queue = new QueueBackend($this->client, $this->queueName, $this->base64); 52 | } 53 | 54 | /** 55 | * @param array $message 56 | * @param int $delay 57 | * @return false|string 58 | */ 59 | public function sendMessage($message, $delay = 0) 60 | { 61 | $message = Json::encode($message); 62 | $request = new SendMessageRequest($message, $delay, null, $this->base64); 63 | try { 64 | $response = $this->queue->sendMessage($request); 65 | if ($response->isSucceed()) { 66 | return $response->getMessageId(); 67 | } else { 68 | return false; 69 | } 70 | } catch (MnsException $e) { 71 | Yii::trace(sprintf('send Message Failed: `%s`...', $e)); 72 | return false; 73 | } 74 | } 75 | 76 | /** 77 | * 批量推送消息到队列 78 | * @param array $messages 79 | * @param int $delay 80 | * @return array 81 | */ 82 | public function batchSendMessage($messages, $delay = 0) 83 | { 84 | foreach ($messages as $key => $message) { 85 | $messages[$key] = new SendMessageRequestItem(Json::encode($message), $delay, null); 86 | } 87 | $count = count($messages); 88 | $size = ceil(count($count) / 16); 89 | $messages = array_chunk($messages, $size); 90 | $responses = []; 91 | foreach ($messages as $message) { 92 | $request = new BatchSendMessageRequest($message, $this->base64); 93 | try { 94 | $response = $this->queue->batchSendMessage($request); 95 | if ($response->isSucceed()) { 96 | $responses = array_merge($responses, $response->getSendMessageResponseItems()); 97 | } 98 | } catch (MnsException $e) { 99 | Yii::trace(sprintf('send Message Failed: `%s`...', $e)); 100 | } 101 | } 102 | return $responses; 103 | } 104 | 105 | /** 106 | * 获取消息 107 | * @return Message|bool 108 | */ 109 | public function receiveMessage() 110 | { 111 | try { 112 | $response = $this->queue->receiveMessage(30); 113 | if ($response->isSucceed()) { 114 | return new Message([ 115 | 'messageBody' => Json::decode($response->getMessageBody()), 116 | 'messageId' => $response->getMessageId(), 117 | 'receiptHandle' => $response->getReceiptHandle(), 118 | 'queue' => $this->queue 119 | ]); 120 | } else { 121 | return false; 122 | } 123 | } catch (MnsException $e) { 124 | Yii::trace(sprintf('receive Message Failed: `%s`...', $e)); 125 | return false; 126 | } 127 | } 128 | 129 | /** 130 | * 修改消息可见时间 131 | * @param string $receiptHandle 132 | * @param int $visibilityTimeout 133 | * @return bool 134 | */ 135 | public function changeMessageVisibility($receiptHandle, $visibilityTimeout) 136 | { 137 | try { 138 | $response = $this->queue->changeMessageVisibility($receiptHandle, $visibilityTimeout); 139 | if ($response->isSucceed()) { 140 | return false; 141 | } else { 142 | return false; 143 | } 144 | } catch (MnsException $e) { 145 | Yii::trace(sprintf('receive Message Failed: `%s`...', $e)); 146 | return false; 147 | } 148 | } 149 | 150 | /** 151 | * 删除消息 152 | * @param string $receiptHandle 153 | * @return bool 154 | */ 155 | public function deleteMessage($receiptHandle) 156 | { 157 | try { 158 | $this->queue->deleteMessage($receiptHandle); 159 | return true; 160 | } catch (MnsException $e) { 161 | Yii::trace(sprintf('Delete Message Failed: `%s`...', $e)); 162 | return false; 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /awsqs/Client.php: -------------------------------------------------------------------------------- 1 | client = new SqsClient($this->client); 32 | } 33 | 34 | /** 35 | * 获取队列 36 | * @param string $queueName 37 | * @return Queue 38 | */ 39 | public function getQueueRef($queueName) 40 | { 41 | return new Queue([ 42 | 'client' => $this->client, 43 | 'queueName' => $queueName, 44 | ]); 45 | } 46 | } -------------------------------------------------------------------------------- /awsqs/Queue.php: -------------------------------------------------------------------------------- 1 | client->sendMessage([ 40 | 'QueueUrl' => $this->queueName, 41 | 'MessageBody' => $message, 42 | 'DelaySeconds' => $delay, 43 | ])->get('MessageId'); 44 | } 45 | 46 | /** 47 | * 获取消息 48 | * @return Message|bool 49 | */ 50 | public function receiveMessage() 51 | { 52 | $response = $this->client->receiveMessage(['QueueUrl' => $this->queueName]); 53 | 54 | if (empty($response['Messages'])) { 55 | return false; 56 | } 57 | $data = reset($response['Messages']); 58 | return new Message([ 59 | 'messageId' => $data['MessageId'], 60 | 'messageBody' => $data['Body'], 61 | 'queue' => $this->queue, 62 | 'receiptHandle' => $data['ReceiptHandle'], 63 | ]); 64 | } 65 | 66 | /** 67 | * 修改消息可见时间 68 | * @param string $receiptHandle 69 | * @param int $visibilityTimeout 70 | * @return bool 71 | */ 72 | public function changeMessageVisibility($receiptHandle, $visibilityTimeout) 73 | { 74 | $this->client->changeMessageVisibility([ 75 | 'QueueUrl' => $this->queueName, 76 | 'ReceiptHandle' => $receiptHandle, 77 | 'VisibilityTimeout' => $visibilityTimeout, 78 | ]); 79 | } 80 | 81 | /** 82 | * 删除消息 83 | * @param string $receiptHandle 84 | * @return bool 85 | */ 86 | public function deleteMessage($receiptHandle) 87 | { 88 | $this->client->deleteMessage([ 89 | 'QueueUrl' => $this->queueName, 90 | 'ReceiptHandle' => $receiptHandle, 91 | ]); 92 | } 93 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xutl/yii2-mq", 3 | "description": "The message queue extension for the Yii framework", 4 | "keywords": [ 5 | "yii2", 6 | "message", 7 | "queue", 8 | "aliyun" 9 | ], 10 | "type": "yii2-extension", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "X TL", 15 | "email": "xtl@gmail.com" 16 | } 17 | ], 18 | "require": { 19 | "yiisoft/yii2": "~2.0.6", 20 | "predis/predis": "^1.1" 21 | }, 22 | "require-dev": { 23 | "xutl/aliyunmns": "~1.0", 24 | "aws/aws-sdk-php": "~3.0", 25 | "predis/predis": "^1.1" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "xutl\\mq\\": "" 30 | } 31 | }, 32 | "repositories": [ 33 | { 34 | "type": "composer", 35 | "url": "https://asset-packagist.org" 36 | }, 37 | { 38 | "type": "composer", 39 | "url": "https://packagist.phpcomposer.com" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /redis/Client.php: -------------------------------------------------------------------------------- 1 | redis === null) { 43 | throw new InvalidConfigException('The "redis" property must be set.'); 44 | } 45 | $this->client = new Redis($this->redis); 46 | } 47 | 48 | /** 49 | * 获取队列 50 | * @param string $queueName 51 | * @return Queue 52 | */ 53 | public function getQueueRef($queueName) 54 | { 55 | return new Queue([ 56 | 'client' => $this->client, 57 | 'queueName' => $queueName, 58 | 'expire' => $this->expire 59 | ]); 60 | } 61 | } -------------------------------------------------------------------------------- /redis/Queue.php: -------------------------------------------------------------------------------- 1 | $id = md5(uniqid('', true)), 'body' => $message]); 45 | if ($delay > 0) { 46 | //放入等待 47 | $this->client->zadd($this->queueName . ':delayed', [$payload => time() + $delay]); 48 | } else { 49 | $this->client->rpush($this->queueName, [$payload]); 50 | } 51 | return $id; 52 | } 53 | 54 | /** 55 | * 获取消息 56 | * @return Message|bool 57 | */ 58 | public function receiveMessage() 59 | { 60 | //遍历保留和等待 61 | foreach ([':delayed', ':reserved'] as $type) { 62 | $options = ['cas' => true, 'watch' => $this->queueName . $type]; 63 | $this->client->transaction($options, function (MultiExec $transaction) use ($type) { 64 | $data = $this->client->zrangebyscore($this->queueName . $type, '-inf', $time = time()); 65 | if (!empty($data)) { 66 | $transaction->zremrangebyscore($this->queueName . $type, '-inf', $time); 67 | //压入队列 68 | foreach ($data as $payload) { 69 | $transaction->rpush($this->queueName, [$payload]); 70 | } 71 | } 72 | }); 73 | } 74 | 75 | $data = $this->client->lpop($this->queueName); 76 | 77 | if ($data === null) { 78 | return false; 79 | } 80 | 81 | $this->client->zadd($this->queueName . ':reserved', [$data => time() + $this->expire]); 82 | 83 | $receiptHandle = $data; 84 | $data = Json::decode($data); 85 | 86 | return new Message([ 87 | 'messageId' => $data['id'], 88 | 'messageBody' => $data['body'], 89 | 'receiptHandle' => $receiptHandle, 90 | 'queue' => $this, 91 | ]); 92 | } 93 | 94 | /** 95 | * 修改消息可见时间 96 | * @param string $receiptHandle 97 | * @param int $visibilityTimeout 98 | * @return bool 99 | */ 100 | public function changeMessageVisibility($receiptHandle, $visibilityTimeout) 101 | { 102 | $this->deleteMessage($receiptHandle); 103 | if ($visibilityTimeout > 0) { 104 | $this->client->zadd($this->queueName . ':delayed', [$receiptHandle => time() + $visibilityTimeout]); 105 | } else { 106 | $this->client->rpush($this->queueName, [$receiptHandle]); 107 | } 108 | } 109 | 110 | /** 111 | * 删除消息 112 | * @param string $receiptHandle 113 | * @return bool 114 | */ 115 | public function deleteMessage($receiptHandle) 116 | { 117 | $this->client->zrem($this->queueName . ':reserved', $receiptHandle); 118 | } 119 | } 120 | --------------------------------------------------------------------------------