// app/Command/QueueWorker.php 队列工作器
<?php
namespace app\Command;
use app\Service\QueueService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\facade\Log;
class QueueWorker extends Command
{
protected function configure()
{
$this
->setName('queue:work')
->setDescription('Process the queue jobs')
->addOption('queue', null, Option::VALUE_REQUIRED, 'The queue to listen on', 'default')
->addOption('daemon', null, Option::VALUE_NONE, 'Run the worker in daemon mode')
->addOption('delay', null, Option::VALUE_OPTIONAL, 'Amount of time to delay failed jobs', 0)
->addOption('memory', null, Option::VALUE_OPTIONAL, 'The memory limit in megabytes', 128)
->addOption('sleep', null, Option::VALUE_OPTIONAL, 'Number of seconds to sleep when no job is available', 3)
->addOption('tries', null, Option::VALUE_OPTIONAL, 'Number of times to attempt a job before logging it failed', 3);
}
protected function execute(Input $input, Output $output)
{
$queueName = $input->getOption('queue');
$isDaemon = $input->getOption('daemon');
$output->writeln("队列工作进程已启动,监听队列: {$queueName}");
Log::info("队列工作进程已启动,监听队列: {$queueName}");
$queueService = new QueueService($queueName);
if ($isDaemon) {
$this->runAsDaemon($queueService, $output);
} else {
$this->runOnce($queueService, $output);
}
}
protected function runOnce(QueueService $queueService, Output $output)
{
$jobData = $queueService->pop();
if ($jobData) {
$output->writeln("处理任务: " . json_encode($jobData['job']));
$this->processJob($jobData);
} else {
$output->writeln("队列为空");
}
}
protected function runAsDaemon(QueueService $queueService, Output $output)
{
while (true) {
try {
$jobData = $queueService->pop();
if ($jobData) {
$output->writeln("处理任务: " . json_encode($jobData['job']));
$this->processJob($jobData);
} else {
// 队列为空,休眠一段时间避免CPU占用过高
sleep(1);
}
} catch (\Exception $e) {
$output->writeln("处理任务时发生错误: " . $e->getMessage());
Log::error("队列处理错误: " . $e->getMessage());
// 发生错误时适当休眠
sleep(5);
}
}
}
protected function processJob($jobData)
{
try {
$jobClass = $jobData['job']['class'];
$jobParams = $jobData['job']['params'];
$jobInstance = new $jobClass(...array_values($jobParams));
$jobInstance->handle();
} catch (\Exception $e) {
Log::error("执行任务失败: " . $e->getMessage());
// 将任务放回队列重试
(new QueueService())->retry($jobData);
}
}
}
// app/Jobs/BatchDownloadJob.php 不同的业务对应不同的job文件
<?php
namespace app\Jobs;
use app\Exception\ServiceException;
use app\Model\System\Attachment;
use app\Model\System\SysDownloadTask;
use app\Service\Compress\ZipService;
use app\Service\QueueService;
use think\facade\Log;
use think\facade\Cache;
class BatchDownloadJob
{
protected $queueService;
public function __construct($data)
{
$this->queueService = new QueueService('batchDownload');
$this->data = $data;
}
public function handle()
{
try {
Log::info("开始下载: {$this->data}");
// 业务逻辑
$this->process(json_decode($this->data, true));
} catch (\Exception $e) {
Log::error("下载任务失败: {$e->getMessage()}");
// 将任务放回队列重试 todo 考虑加重试限制
// return $this->queueService->retry([
// 'job' => [
// 'class' => self::class,
// 'params' => [
// 'data' => $this->data,
// ]
// ],
// 'attempts' => 0
// ]);
}
}
private function process(array $data)
{
// 业务逻辑方法 coding
}
}
// config/console.php. 这部分命令行执行用,新增业务不需要修改这部分
<?php
// +----------------------------------------------------------------------
// | 控制台配置
// +----------------------------------------------------------------------
use app\Command\QueueWorker;
return [
// 指令定义
'commands' => [
'queue:work' => QueueWorker::class,
],
];
// app/Service/QueueService.php 队列处理,新增业务不需要修改这部分
<?php
namespace app\Service;
use think\facade\Cache;
use think\facade\Log;
class QueueService
{
protected $queueName;
protected $driver;
protected $retryTimes = 3;
protected $retryDelay = 60;
public function __construct($queueName = 'default')
{
$this->queueName = $queueName;
$this->driver = $this->getQueueDriver();
}
protected function getQueueDriver()
{
// 根据配置获取队列驱动,这里简化为直接返回Redis驱动
return Cache::store('redis');
}
public function push($job)
{
$payload = json_encode([
'job' => $job,
'attempts' => 0,
'created_at' => time()
]);
return $this->driver->rPush($this->getQueueKey(), $payload);
}
public function pop()
{
$payload = $this->driver->lPop($this->getQueueKey());
if ($payload) {
return json_decode($payload, true);
}
return null;
}
public function size()
{
return $this->driver->lLen($this->getQueueKey());
}
public function retry($jobData)
{
$jobData['attempts']++;
if ($jobData['attempts'] <= $this->retryTimes) {
// 延迟重试
sleep($this->retryDelay);
return $this->push($jobData['job']);
} else {
// 重试次数耗尽,记录失败日志
Log::error('Job failed after retries', [
'job' => $jobData['job'],
'attempts' => $jobData['attempts']
]);
return false;
}
}
protected function getQueueKey()
{
return 'queue:' . $this->queueName;
}
}
// 使用示例
$job = [
'class' => BatchDownloadJob::class,
'params' => [
'data' => json_encode(['task_id' => $taskId], true),
]
];
// 将任务推送到队列
$queueService = new QueueService('batchDownload');
$sendResult = $queueService->push($job);
// 然后新增 Jobs 下的业务job文件,该文件中 必须要有 handle 方法
// 使用命令 php think queue:work --queue=batchDownload --daemon [ 其中batchDownload属于业务的队列名称 ]