这里使用的是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());
}
}
}
流程图: