利用Swoole+Redis實現隊列功能

這裏使用的是Yii2框架

1.在console文件夾下創建startSwoole.php

<?php

date_default_timezone_set('Asia/Shanghai');
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');
defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
defined('STDOUT') or define('STDOUT', fopen('php://stdout', 'w'));
// Composer
require(__DIR__ . '/../vendor/autoload.php');

// Environment
require(__DIR__ . '/../common/env.php');

// Yii
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');

// Bootstrap application
require(__DIR__ . '/../common/config/bootstrap.php');
require(__DIR__ . '/config/bootstrap.php');
//Redis操作類
require_once('RedisCacheClass.php');
//Swoole異步服務器
require_once('SwooleProcess.php');
new \console\SwooleProcess();

這個文件是用來啓動我們的swoole服務的。

2.在console目錄下創建SwooleProcess.php和RedisCacheClass.php文件

【SwooleProcess.php】

<?php

namespace console;

use Swoole\Process;
use yii\console\Application;
use yii\helpers\ArrayHelper;
use Yii;

/**
 * 繼承並實現方法以開發基於swoole的多進程應用
 * Class ProcessTask
 */
class SwooleProcess
{
	/**
	 * 程序是否以守護者進程方式運行
	 * @var bool
	 */
	public $daemon = true;
	/**
	 * 主進程pid
	 * @var int
	 */
	public $masterPid = 0;
	/**
	 * 最大進程數
	 * @var int
	 */
	public $maxProcess = 1;
	/**
	 * 子進程信息
	 * @var array
	 */
	public $workers = [];

	public function __construct()
	{
		try {
			if (!extension_loaded('swoole')) {
				die('not install Swoole extension');
			}
			if (!is_dir(__DIR__ . "/runtime/logs")) {
				mkdir(__DIR__ . "/runtime/logs");
				if (!file_exists(__DIR__ . "/runtime/logs/pushQueue.log")) {
					file_put_contents(__DIR__ . "/runtime/logs/pushQueue.log", '');
				}
			}
			if ($this->daemon) {
				Process::daemon();
			}
			$this->setProcessName(sprintf('php-ps:%s', 'master'));
			$this->masterPid = posix_getpid();
			$this->run();
			$this->processWait();
		} catch (\Exception $exception) {
			Yii::error($exception->__toString(), 'kafka');
			$this->err($exception->getMessage());
		}
	}

	/**
	 * 進程池入口
	 */
	public function run()
	{
		for ($i = 0; $i < $this->maxProcess; $i++) {
			$this->createProcessQueueTrue($i);
			$this->createProcessQueueFail($i);
		}
	}

	/**
	 * 創建新進程-排隊
	 * @param $index
	 * @return int
	 */
	public function createProcessQueueTrue($index)
	{
		$process = new Process(function (Process $worker) use ($index) {
			$this->setProcessName(sprintf('php-ps:%s', $index) . '-排隊');
			Yii::error($worker->pid . ':開啓進程', 'kafka');
			$this->printToScreen($worker->pid . ':開啓進程queue');
			try {
				//外層循環,使kafka出現問題時能重新初始化
				while (true) {
					//檢測父進程是否已退出,父進程退出則子進程退出
					$this->checkMasterPid($worker);
					$this->startProcessIsQueue();
					//10s後重新初始化kafka消費者
					sleep(10);
				}
			} catch (Exception $exception) {
				Yii::error($worker->pid . ':' . $exception->__toString(), 'kafka');
				$this->printToScreen($worker->pid . ':' . $exception->__toString(), 'error');
				sleep(10);
			}
			Yii::error($worker->pid . ':進程結束', 'kafka');
			$this->printToScreen($worker->pid . ':進程結束');
			$worker->exit(0);
		}, false, false);
		$pid = $process->start();
		$this->workers[$index] = $pid;
		return $pid;
	}

	/**
	 * 創建新進程-不排隊
	 * @param $index
	 * @return int
	 */
	public function createProcessQueueFail($index)
	{
		$process = new Process(function (Process $worker) use ($index) {
			$this->setProcessName(sprintf('php-ps:%s', $index) . '-不排隊');
			Yii::error($worker->pid . ':開啓進程', 'kafka');
			$this->printToScreen($worker->pid . ':開啓進程no-queue');
			try {
				//外層循環,使kafka出現問題時能重新初始化
				while (true) {
					//檢測父進程是否已退出,父進程退出則子進程退出
					$this->checkMasterPid($worker);
					$this->startProcessNoQueue();
					//10s後重新初始化kafka消費者
					sleep(10);
				}
			} catch (Exception $exception) {
				Yii::error($worker->pid . ':' . $exception->__toString(), 'kafka');
				$this->printToScreen($worker->pid . ':' . $exception->__toString(), 'error');
				sleep(10);
			}
			Yii::error($worker->pid . ':進程結束', 'kafka');
			$this->printToScreen($worker->pid . ':進程結束');
			$worker->exit(0);
		}, false, false);
		$pid = $process->start();
		$this->workers[$index] = $pid;
		return $pid;
	}

	/**
	 * 檢測父進程是否已退出
	 * @param Process $worker
	 */
	public function checkMasterPid(Process &$worker)
	{
		if (!\Swoole\Process::kill($this->masterPid, 0)) {
			$this->printToScreen($worker->pid . ':父進程已退出,子進程退出');
			Yii::error($worker->pid . ':父進程已退出,子進程退出', 'kafka');
			$worker->exit(0);
		}
	}

	/**
	 * 重啓進程
	 * @param $ret
	 * @throws Exception
	 */
	public function rebootProcess($ret)
	{
		$index = array_search($ret['pid'], $this->workers);
		if ($index !== false) {
			$newPid = $this->createProcess($index);
			Yii::error($newPid . ':重啓進程', 'kafka');
			$this->printToScreen($newPid . ':重啓進程');
		} else {
			throw new \Exception('rebootProcess Error: no pid');
		}
	}

	/**
	 * 處理殭屍進程,並重啓進程
	 * @throws Exception
	 */
	public function processWait()
	{
		while (true) {
			if (count($this->workers)) {
				$ret = Process::wait();
				if ($ret) {
					$this->rebootProcess($ret);
				}
			} else {
				break;
			}
		}
	}

	/**
	 * 設置進程名
	 * @param $name
	 */
	public function setProcessName($name)
	{
		if (function_exists('cli_set_process_title')) {
			cli_set_process_title($name);
		} else {
			swoole_set_process_name($name);
		}
	}

	/**
	 * 輸出到屏幕
	 * @param $message
	 * @param string $type
	 * @param bool $newLine
	 */
	public function printToScreen($message, $type = 'out', $newLine = true)
	{
		if (!$this->daemon) {
			if ($type == 'error') {
				Yii::error($message);
			} else {
				echo $message;
			}
		}
	}

	/**
	 * 用戶的邏輯-需要排隊
	 * @return mixed
	 */
	public function startProcessIsQueue()
	{
		try {
			$config = ArrayHelper::merge(
				require(__DIR__ . '/../common/config/base.php'),
				require(__DIR__ . '/../common/config/console.php'),
				require(__DIR__ . '/config/console.php')
			);
			$config['bootstrap'] = ['log'];
			$app = new Application($config);
			$app->runAction('push-queue/yes-queue', []);
			$app->getDb()->close();
			unset($app);
		} catch (\Exception $exception) {
			$this->printToScreen($exception->getMessage());
			if (isset($app)) {
				unset($app);
			}
			$finishData['status'] = 'fail';
			$finishData['exception'] = $exception->__toString();
		}
	}
	/**
	 * 用戶的邏輯-不需要排隊
	 * @return mixed
	 */
	public function startProcessNoQueue()
	{
		try {
			$config = ArrayHelper::merge(
				require(__DIR__ . '/../common/config/base.php'),
				require(__DIR__ . '/../common/config/console.php'),
				require(__DIR__ . '/config/console.php')
			);
			$config['bootstrap'] = ['log'];
			$app = new Application($config);
			$app->runAction('push-queue/no-queue', []);
			$app->getDb()->close();
			unset($app);
		} catch (\Exception $exception) {
			$this->printToScreen($exception->getMessage());
			if (isset($app)) {
				unset($app);
			}
			$finishData['status'] = 'fail';
			$finishData['exception'] = $exception->__toString();
		}
	}
}

【RedisCacheClass.php】

<?php

namespace console;
class RedisCacheClass
{
	/**
	 * @var $redis \Redis
	 */
	private $redis;
	private $config;

	public function __construct($config)
	{
		if (empty($config['REDIS_SCHEME'])) {
			$config['REDIS_SCHEME'] = 'tcp';
		}
		if (empty($config['REDIS_HOST'])) {
			throw new \Exception('Redis Host Is Empty');
		}
		if (empty($config['REDIS_PORT'])) {
			$config['REDIS_PORT'] = '6379';
		}
		if (!isset($config['REDIS_PASSWORD'])) {
			$config['REDIS_PASSWORD'] = null;
		}
		if (!isset($config['REDIS_DB'])) {
			$config['REDIS_DB'] = 0;
		}
		if (!isset($config['REDIS_PREFIX'])) {
			$config['REDIS_PREFIX'] = 'PHPREDIS_CACHE:';
		}

		if (!isset($config['REDIS_PERSISTENT'])) {
			$config['REDIS_PERSISTENT'] = false;
		}
		$this->redis = new \Redis();
		if ($config['REDIS_PERSISTENT']) {
			$this->redis->pconnect($config['REDIS_HOST'], $config['REDIS_PORT']);
		} else {
			$this->redis->connect($config['REDIS_HOST'], $config['REDIS_PORT']);
		}
		$this->redis->auth($config['REDIS_PASSWORD']);
		$this->redis->select($config['REDIS_DB']);
		$this->redis->setOption(\Redis::OPT_PREFIX, $config['REDIS_PREFIX']);
	}

	public function set($key, $value, $duration = 60)
	{
		return $this->redis->setex($key, $duration, $value);
	}

	public function get($key, $value = '')
	{
		$redisValue = $this->redis->get($key);
		if (empty($redisValue)) {
			return $value;
		}
		return $redisValue;
	}

	public function del($key)
	{
		return $this->redis->del($key);
	}

	public function close()
	{
		if($this->redis->ping()) {
			$this->redis->close();
		}
	}

	public function status()
	{
		try {
			return $this->redis->ping();
		} catch (\Exception $exception) {
			return false;
		}
	}

	/**
	 * 設置Redis Hash緩存
	 * @param $key
	 * @param $hashKey
	 * @param $value
	 */
	public function hMset($key, $hashKey, $value)
	{
		$this->redis->hMset($key, array($hashKey => serialize($value)));
	}

	/**
	 * 獲取Redis Hash緩存
	 * @param $key
	 * @param $hashKey
	 * @return array|null
	 */
	public function hMget($key, $hashKey)
	{
		if (!$this->redis->hExists($key, $hashKey)) {
			return null;
		}
		if (!is_array($hashKey)) {
			$hash = $this->redis->hMget($key, array($hashKey));
			if (isset($hash[$hashKey])) {
				return $hash[$hashKey];
			} else {
				return null;
			}
		} else {
			$hash = $this->redis->hMget($key, $hashKey);
		}
		return $hash;
	}

	/**
	 * 刪除Redis Hash緩存
	 * @param $key
	 * @param $hashKey
	 * @return bool|int
	 */
	public function hDel($key, $hashKey)
	{
		if (!$this->redis->hExists($key, $hashKey)) {
			return true;
		}
		return $this->redis->hDel($key, $hashKey);
	}
}

在console/controller中建立PushQueueController.php文件

<?php

namespace console\controllers;

use api\models\Queue;
use api\models\WorkflowSetup;
use common\components\Curl\Curl;
use console\controllers\AppController;
use Yii;

class PushQueueController extends AppController
{
	/**
	 * 排隊進程異步消息通知
	 *
	 * @return void
	 */
	public function actionYesQueue()
	{
		try {
			$obj = Queue::find()->where(['status' => Queue::UNCOMMITTED])->orderBy(['priority' => SORT_DESC, 'create_time' => SORT_ASC])->limit(1)->one();
			if (!$obj) {
				return;
			}
			$WorkflowSetupObj = WorkflowSetup::find()->where(['model_key' => $obj->model_key, 'tenant_id' => $obj->tenant_id])->limit(1)->one();
			if (!$WorkflowSetupObj) {
				throw new \Exception('模板數據不存在');
			}
			if (Yii::$app->redis->set('QUEUE_TEMPLATE_TASK:QUEUE_LOCK', 'QUEUE_LOCK', 'NX')) {
				try {
					if ($WorkflowSetupObj->is_queue == WorkflowSetup::IS_QUEUE_TRUE) {
						$currentTaskCount = Yii::$app->redis->get('QUEUE_TEMPLATE_TASK:currentTaskCount#' . $obj->tenant_id . '#' . $obj->model_key) ?: 0;
						if ($currentTaskCount < $WorkflowSetupObj->queue_size) {
							$sendData = ['piid' => $obj->piid, 'stepName' => 'queue', 'actionType' => 'success', 'data' => $obj->business_data];
							$result = Curl::takeCurl('post', $obj->callback_url, $sendData);
							if (!$result) {
								throw new \Exception("未收到工作流callback地址:$obj->callback_url 返回的數據");
							}
							$result = json_decode($result, true);
							if ($result['code'] == WorkflowSetup::SUCCESS) {
								$obj->status = Queue::COMMITTING;
								if ($obj->save()) {
									$currentTaskCount = $currentTaskCount + 1;
									Yii::$app->redis->set('QUEUE_TEMPLATE_TASK:currentTaskCount#' . $obj->tenant_id . '#' . $obj->model_key, $currentTaskCount);
								}
							} else {
								throw new \Exception("回調失敗,返回結果狀態錯誤");
							}
						}
					}
					Yii::$app->redis->del('QUEUE_TEMPLATE_TASK:QUEUE_LOCK');
				} catch (\Exception $e) {
					Yii::$app->redis->del('QUEUE_TEMPLATE_TASK:QUEUE_LOCK');
					throw $e;
				}
			}
		} catch (\Exception $exception) {
			\Yii::error($exception->__toString());
		}
	}

	/**
	 * 不排隊進程異步消息通知
	 *
	 * @return void
	 */
	public function actionNoQueue()
	{
		try {
			sleep(3);
			$obj = Queue::find()->where(['status' => Queue::NO_QUEUE])->orderBy(['create_time' => SORT_ASC])->limit(1)->one();
			if (!$obj) {
				return;
			}
			$business_data = json_decode($obj->business_data, true);
			$business_data['ROOT']['WorkFlow']['queue'] = 'false';
			$business_data = json_encode($business_data);
			$successData = ['stepName' => 'queue', 'actionType' => 'success', 'data' => $business_data];
			$result = Curl::takeCurl('post', $obj->callback_url, $successData);
			if (!$result) {
				throw new \Exception("未收到工作流callback地址:$obj->callback_url 返回的數據");
			}
			$result = json_decode($result, true);
			if ($result['code'] == WorkflowSetup::SUCCESS) {
				$obj->delete();
			} else {
				throw new \Exception("回調失敗,返回結果狀態錯誤");
			}
		} catch (\Exception $exception) {
			\Yii::error($exception->__toString());
		}
	}
}

流程圖:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章