PHP定时任务调度系统

wen PHP项目 1

本文目录导读:

PHP定时任务调度系统

  1. 系统架构
  2. 核心代码实现
  3. 使用说明
  4. 监控和通知
  5. 性能优化建议
  6. 安全考虑

我将为您设计一个完整的PHP定时任务调度系统,这个系统将包含任务管理、调度执行、日志记录等核心功能。

系统架构

├── cron/
│   ├── config/
│   │   └── tasks.php          # 任务配置文件
│   ├── lib/
│   │   ├── Logger.php         # 日志系统
│   │   ├── TaskManager.php    # 任务管理器
│   │   ├── Scheduler.php      # 调度器
│   │   └── Task.php           # 任务类
│   ├── logs/                  # 日志目录
│   └── run.php               # 入口脚本

核心代码实现

1 任务类 (Task.php)

<?php
namespace CronSystem;
/**
 * 任务实体类
 */
class Task
{
    private $name;
    private $command;
    private $schedule;
    private $timeout;
    private $status;
    private $lastRunTime;
    private $lastRunResult;
    private $retryCount;
    private $failCount;
    private $maxRetries;
    public function __construct(array $config)
    {
        $this->name = $config['name'] ?? '';
        $this->command = $config['command'] ?? '';
        $this->schedule = $config['schedule'] ?? '* * * * *';
        $this->timeout = $config['timeout'] ?? 300;
        $this->status = 'pending';
        $this->lastRunTime = null;
        $this->lastRunResult = null;
        $this->retryCount = 0;
        $this->failCount = 0;
        $this->maxRetries = $config['max_retries'] ?? 3;
    }
    // 检查任务是否应该在当前时间执行
    public function isDue(\DateTime $currentTime): bool
    {
        return $this->cronExpressionMatches($currentTime);
    }
    // 解析cron表达式并检查是否匹配
    private function cronExpressionMatches(\DateTime $time): bool
    {
        $parts = explode(' ', $this->schedule);
        if (count($parts) !== 5) {
            return false;
        }
        $minute = $time->format('i');
        $hour = $time->format('G');
        $day = $time->format('j');
        $month = $time->format('n');
        $weekday = $time->format('w');
        return $this->expressionMatch($parts[0], (int)$minute) &&
               $this->expressionMatch($parts[1], (int)$hour) &&
               $this->expressionMatch($parts[2], (int)$day) &&
               $this->expressionMatch($parts[3], (int)$month) &&
               $this->expressionMatch($parts[4], (int)$weekday);
    }
    // 匹配单个cron字段
    private function expressionMatch(string $expression, int $value): bool
    {
        if ($expression === '*') return true;
        // 处理逗号分隔的多个值
        if (strpos($expression, ',') !== false) {
            $values = explode(',', $expression);
            foreach ($values as $val) {
                if ($this->expressionMatch(trim($val), $value)) return true;
            }
            return false;
        }
        // 处理范围值
        if (strpos($expression, '-') !== false) {
            list($start, $end) = explode('-', $expression);
            return $value >= (int)$start && $value <= (int)$end;
        }
        // 处理步长值
        if (strpos($expression, '/') !== false) {
            list($base, $step) = explode('/', $expression);
            if ($base === '*') $base = 0;
            return ($value >= (int)$base) && (($value - (int)$base) % (int)$step === 0);
        }
        // 精确匹配
        return (int)$expression === $value;
    }
    // Getters and Setters
    public function getName() { return $this->name; }
    public function getCommand() { return $this->command; }
    public function getSchedule() { return $this->schedule; }
    public function getTimeout() { return $this->timeout; }
    public function getStatus() { return $this->status; }
    public function setStatus($status) { $this->status = $status; }
    public function setLastRunTime($time) { $this->lastRunTime = $time; }
    public function setLastRunResult($result) { $this->lastRunResult = $result; }
    public function incrementRetry() { $this->retryCount++; }
    public function getRetryCount() { return $this->retryCount; }
    public function getMaxRetries() { return $this->maxRetries; }
    public function incrementFailCount() { $this->failCount++; }
    public function getFailCount() { return $this->failCount; }
}

2 任务管理器 (TaskManager.php)

<?php
namespace CronSystem;
/**
 * 任务管理器 - 负责加载和保存任务配置
 */
class TaskManager
{
    private $tasks = [];
    private $configFile;
    private $stateFile;
    public function __construct(string $configDir)
    {
        $this->configFile = $configDir . '/tasks.php';
        $this->stateFile = $configDir . '/runtime_state.json';
        $this->loadTasks();
        $this->loadState();
    }
    // 加载任务配置
    private function loadTasks(): void
    {
        if (file_exists($this->configFile)) {
            $configs = require $this->configFile;
            foreach ($configs as $config) {
                $this->tasks[$config['name']] = new Task($config);
            }
        }
    }
    // 加载运行状态
    private function loadState(): void
    {
        if (file_exists($this->stateFile)) {
            $state = json_decode(file_get_contents($this->stateFile), true);
            if (is_array($state)) {
                foreach ($state as $name => $taskState) {
                    if (isset($this->tasks[$name])) {
                        $this->tasks[$name]->setStatus($taskState['status'] ?? 'pending');
                        $this->tasks[$name]->setLastRunTime($taskState['last_run_time'] ?? null);
                        if (isset($taskState['fail_count'])) {
                            // 这里简化为记录失败次数,完整实现需要更多字段
                        }
                    }
                }
            }
        }
    }
    // 保存运行状态
    public function saveState(): void
    {
        $state = [];
        foreach ($this->tasks as $name => $task) {
            $state[$name] = [
                'status' => $task->getStatus(),
                'last_run_time' => $task->getLastRunTime(),
            ];
        }
        file_put_contents($this->stateFile, json_encode($state, JSON_PRETTY_PRINT));
    }
    // 获取所有任务
    public function getTasks(): array
    {
        return $this->tasks;
    }
    // 获取特定任务
    public function getTask(string $name): ?Task
    {
        return $this->tasks[$name] ?? null;
    }
    // 查找需要执行的任务
    public function findDueTasks(\DateTime $time): array
    {
        $dueTasks = [];
        foreach ($this->tasks as $task) {
            if ($task->isDue($time)) {
                $dueTasks[] = $task;
            }
        }
        return $dueTasks;
    }
    // 添加新任务
    public function addTask(array $config): bool
    {
        if (isset($this->tasks[$config['name']])) {
            return false;
        }
        $task = new Task($config);
        $this->tasks[$config['name']] = $task;
        $this->persistTasks();
        return true;
    }
    // 更新任务
    public function updateTask(string $name, array $config): bool
    {
        if (!isset($this->tasks[$name])) {
            return false;
        }
        $this->tasks[$name] = new Task(array_merge(
            ['name' => $name],
            $config
        ));
        $this->persistTasks();
        return true;
    }
    // 删除任务
    public function deleteTask(string $name): bool
    {
        if (!isset($this->tasks[$name])) {
            return false;
        }
        unset($this->tasks[$name]);
        $this->persistTasks();
        return true;
    }
    // 持久化任务配置
    private function persistTasks(): void
    {
        $taskConfigs = [];
        foreach ($this->tasks as $task) {
            $taskConfigs[] = [
                'name' => $task->getName(),
                'command' => $task->getCommand(),
                'schedule' => $task->getSchedule(),
                'timeout' => $task->getTimeout(),
            ];
        }
        $content = "<?php return " . var_export($taskConfigs, true) . ";";
        file_put_contents($this->configFile, $content);
    }
}

3 调度器 (Scheduler.php)

<?php
namespace CronSystem;
/**
 * 调度器 - 核心调度逻辑
 */
class Scheduler
{
    private $taskManager;
    private $logger;
    private $isRunning = false;
    private $processes = [];
    public function __construct(TaskManager $taskManager, Logger $logger)
    {
        $this->taskManager = $taskManager;
        $this->logger = $logger;
    }
    // 执行一次调度检查
    public function run(): void
    {
        $currentTime = new \DateTime();
        $this->logger->info("Scheduler check at: " . $currentTime->format('Y-m-d H:i:s'));
        $dueTasks = $this->taskManager->findDueTasks($currentTime);
        foreach ($dueTasks as $task) {
            $this->executeTask($task);
        }
        $this->taskManager->saveState();
        $this->cleanupFinishedProcesses();
    }
    // 执行单个任务
    private function executeTask(Task $task): void
    {
        // 防止重复执行
        if ($task->getStatus() === 'running') {
            $this->logger->warning("Task {$task->getName()} already running, skipping");
            return;
        }
        $this->logger->info("Starting task: {$task->getName()}");
        // 设置任务状态
        $task->setStatus('running');
        $task->setLastRunTime(date('Y-m-d H:i:s'));
        // 创建子进程执行任务
        $processId = pcntl_fork();
        if ($processId === -1) {
            // fork失败
            $this->logger->error("Failed to fork process for task: {$task->getName()}");
            $task->setStatus('failed');
            $task->incrementFailCount();
            return;
        } elseif ($processId === 0) {
            // 子进程执行
            try {
                $this->executeCommand($task);
                exit(0);
            } catch (\Exception $e) {
                $this->logger->error("Task execution failed: " . $e->getMessage());
                exit(1);
            }
        } else {
            // 父进程记录子进程信息
            $this->processes[$processId] = [
                'task' => $task,
                'started_at' => time()
            ];
        }
    }
    // 实际执行命令
    private function executeCommand(Task $task): void
    {
        // 检查并清理子进程资源
        $this->cleanupProcesses();
        // 设置超时
        $process = proc_open(
            $task->getCommand(),
            [
                0 => ['file', '/dev/null', 'r'],
                1 => ['file', '/tmp/task_output.txt', 'w'],
                2 => ['file', '/tmp/task_error.txt', 'a']
            ],
            $pipes
        );
        if (is_resource($process)) {
            $status = proc_get_status($process);
            $startTime = time();
            // 等待进程完成或超时
            while ($status['running']) {
                usleep(100000); // 100ms
                $status = proc_get_status($process);
                // 检查超时
                if ((time() - $startTime) > $task->getTimeout()) {
                    $this->logger->error("Task {$task->getName()} timed out");
                    proc_terminate($process);
                    $task->setStatus('timeout');
                    $task->incrementFailCount();
                    return;
                }
            }
            // 进程完成
            $exitCode = $status['exitcode'];
            if ($exitCode === 0) {
                $task->setStatus('success');
                $task->setLastRunResult('Success');
            } else {
                $task->setStatus('failed');
                $task->incrementFailCount();
                $task->setLastRunResult('Failed with exit code: ' . $exitCode);
                // 重试机制
                if ($task->getRetryCount() < $task->getMaxRetries()) {
                    $this->retryTask($task);
                }
            }
            proc_close($process);
            // 记录完成时间
            $this->logger->info(
                "Task {$task->getName()} completed with status: {$task->getStatus()} , 耗时: " . (time() - $startTime) . "s"
            );
        }
    }
    // 重试机制
    private function retryTask(Task $task): void
    {
        $this->logger->info("Retrying task: {$task->getName()} ({$task->getRetryCount()}/{$task->getMaxRetries()})");
        $retryDelay = pow(2, $task->getRetryCount()) * 5; // 指数退避
        sleep($retryDelay);
        if ($task->isDue(new \DateTime())) {
            $task->incrementRetry();
            $this->executeTask($task);
        }
    }
    // 清理已完成进程
    private function cleanupProcesses(): void
    {
        foreach ($this->processes as $pid => $processInfo) {
            $res = pcntl_waitpid($pid, $status, WNOHANG);
            if ($res > 0 || $res === -1) {
                unset($this->processes[$pid]);
            }
        }
    }
    // 清理所有资源
    public function shutdown(): void
    {
        foreach ($this->processes as $pid => $processInfo) {
            // 强制终止还在运行的进程
            posix_kill($pid, SIGKILL);
        }
        $this->logger->info("Scheduler shutdown complete");
    }
    // 获取所有任务状态
    public function getTaskStatuses(): array
    {
        $statuses = [];
        $tasks = $this->taskManager->getTasks();
        foreach ($tasks as $task) {
            $statuses[$task->getName()] = [
                'status' => $task->getStatus(),
                'last_run' => $task->getLastRunTime(),
                'last_result' => $task->getLastRunResult(),
                'fail_count' => $task->getFailCount(),
            ];
        }
        return $statuses;
    }
}

4 日志系统 (Logger.php)

<?php
namespace CronSystem;
/**
 * 日志系统 - 支持分级日志和文件输出
 */
class Logger
{
    private $logFile;
    private $logLevel;
    private $maxFileSize = 10485760; // 10MB
    const LEVELS = [
        'debug' => 100,
        'info' => 200,
        'warning' => 300,
        'error' => 400,
        'critical' => 500
    ];
    public function __construct(string $logDir, string $level = 'info')
    {
        $this->logFile = $logDir . '/cron_' . date('Y-m-d') . '.log';
        $this->logLevel = $level;
        // 确保日志目录存在
        if (!is_dir($logDir)) {
            mkdir($logDir, 0755, true);
        }
        // 日志文件轮转
        $this->rotateLogFile();
    }
    public function debug(string $message): void
    {
        $this->log('debug', $message);
    }
    public function info(string $message): void
    {
        $this->log('info', $message);
    }
    public function warning(string $message): void
    {
        $this->log('warning', $message);
    }
    public function error(string $message): void
    {
        $this->log('error', $message);
    }
    public function critical(string $message): void
    {
        $this->log('critical', $message);
    }
    private function log(string $level, string $message): void
    {
        // 检查日志级别
        if (self::LEVELS[$level] < self::LEVELS[$this->logLevel]) {
            return;
        }
        $timestamp = date('Y-m-d H:i:s');
        $logMessage = sprintf(
            "[%s] [%s] [PID:%d] %s\n",
            $timestamp,
            strtoupper($level),
            getmypid(),
            $message
        );
        file_put_contents($this->logFile, $logMessage, FILE_APPEND | LOCK_EX);
    }
    // 日志文件轮转
    private function rotateLogFile(): void
    {
        if (file_exists($this->logFile) && filesize($this->logFile) >= $this->maxFileSize) {
            $backupFile = $this->logFile . '.' . date('His');
            rename($this->logFile, $backupFile);
        }
    }
    // 获取日志内容
    public function getLogs(int $lines = 100): array
    {
        if (!file_exists($this->logFile)) {
            return [];
        }
        $content = file($this->logFile);
        return array_slice($content, -$lines);
    }
}

5 入口脚本 (run.php)

<?php
require_once __DIR__ . '/lib/Logger.php';
require_once __DIR__ . '/lib/Task.php';
require_once __DIR__ . '/lib/TaskManager.php';
require_once __DIR__ . '/lib/Scheduler.php';
use CronSystem\Logger;
use CronSystem\TaskManager;
use CronSystem\Scheduler;
// 基础配置
$configDir = __DIR__ . '/config';
$logDir = __DIR__ . '/logs';
// 初始化系统
$logger = new Logger($logDir, 'info');
$taskManager = new TaskManager($configDir);
$scheduler = new Scheduler($taskManager, $logger);
// 处理命令行参数
$options = getopt('h', ['help', 'run', 'list', 'status', 'add:', 'delete:', 'update:']);
if (isset($options['h']) || isset($options['help'])) {
    echo <<<HELP
    PHP Task Scheduler
    Usage: php run.php [options]
    Options:
      --run        Run scheduler once
      --list       List all tasks
      --status     Show task statuses
      --add=JSON   Add new task (synchronous)
      --delete=NAME Delete task
      --update=NAME=JSON Update task
      --help       Show this help
    HELP;
    exit(0);
}
// 主调度循环
if (isset($options['run'])) {
    $logger->info("Starting scheduler in run-once mode");
    // 信号处理
    pcntl_async_signals(true);
    pcntl_signal(SIGTERM, function() use ($scheduler, $logger) {
        $logger->info("Received SIGTERM, shutting down...");
        $scheduler->shutdown();
        exit(0);
    });
    pcntl_signal(SIGINT, function() use ($scheduler, $logger) {
        $logger->info("Received SIGINT, shutting down...");
        $scheduler->shutdown();
        exit(0);
    });
    // 执行调度
    $scheduler->run();
    // 可选:持续运行模式
    $continuous = false;
    if ($continuous) {
        while (true) {
            sleep(60);
            $scheduler->run();
        }
    }
} elseif (isset($options['list'])) {
    // 列出任务
    $tasks = $taskManager->getTasks();
    echo "Task Listing:\n";
    echo "=============\n";
    foreach ($tasks as $task) {
        echo sprintf(
            "Name: %-20s Schedule: %-15s Status: %-10s\n",
            $task->getName(),
            $task->getSchedule(),
            $task->getStatus()
        );
    }
} elseif (isset($options['status'])) {
    // 显示任务状态
    echo "Task Status Report\n";
    echo "==================\n";
    $statuses = $scheduler->getTaskStatuses();
    foreach ($statuses as $name => $status) {
        echo "{$name}:\n";
        echo "  Status: {$status['status']}\n";
        echo "  Last Run: {$status['last_run']}\n";
        echo "  Result: {$status['last_result']}\n";
        echo "  Fail Count: {$status['fail_count']}\n";
        echo "  --------------------\n";
    }
} elseif (isset($options['add'])) {
    // 添加新任务
    $config = json_decode($options['add'], true);
    if ($taskManager->addTask($config)) {
        echo "Task added successfully\n";
    } else {
        echo "Failed to add task\n";
    }
} elseif (isset($options['delete'])) {
    // 删除任务
    $taskName = $options['delete'];
    if ($taskManager->deleteTask($taskName)) {
        echo "Task deleted successfully\n";
    } else {
        echo "Task not found or deletion failed\n";
    }
} elseif (isset($options['update'])) {
    // 更新任务
    $parts = explode('=', $options['update'], 2);
    $taskName = $parts[0];
    $config = json_decode($parts[1], true);
    if ($taskManager->updateTask($taskName, $config)) {
        echo "Task updated successfully\n";
    } else {
        echo "Task update failed\n";
    }
} else {
    echo "Invalid usage. Use --help for more information.\n";
    exit(1);
}

6 配置文件示例 (tasks.php)

<?php
return [
    [
        'name' => 'backup_database',
        'command' => 'php /path/to/backup_script.php',
        'schedule' => '0 2 * * *',          // 每天凌晨2点
        'timeout' => 600,
        'max_retries' => 3,
    ],
    [
        'name' => 'clean_temp_files',
        'command' => 'rm -rf /tmp/temp_*',
        'schedule' => '*/30 * * * *',       // 每30分钟
        'timeout' => 60,
        'max_retries' => 2,
    ],
    [
        'name' => 'update_indexes',
        'command' => 'php /path/to/update_scripts.php --type=index',
        'schedule' => '0 */12 * * *',       // 每12小时
        'timeout' => 1200,
        'max_retries' => 5,
    ],
    [
        'name' => 'send_email_digest',
        'command' => 'php /path/to/send_digest.php',
        'schedule' => '0 8 * * 1-5',       // 工作日早上8点
        'timeout' => 300,
        'max_retries' => 3,
    ],
    [
        'name' => 'process_webhooks',
        'command' => 'php /path/to/process_webhooks.php',
        'schedule' => '*/5 * * * *',        // 每5分钟
        'timeout' => 120,
        'max_retries' => 5,
    ],
];

使用说明

1 安装和配置

  1. 将上述代码保存到对应的文件路径中
  2. 确保PHP环境支持pcntlposix扩展
  3. config/tasks.php中配置你的定时任务

2 常用命令

# 运行一次调度检查
php run.php --run
# 查看任务列表
php run.php --list
# 添加新任务
php run.php --add='{"name":"test_task","command":"php script.php","schedule":"* * * * *"}'
# 删除任务
php run.php --delete=test_task
# 更新任务
php run.php --update='test_task={"schedule":"*/5 * * * *"}'
# 查看任务状态
php run.php --status
# 显示帮助
php run.php --help

3 使用 Cron 集成

在系统的 crontab 中添加以下配置:

# 每5分钟运行一次调度器
*/5 * * * * /usr/bin/php /path/to/cron/run.php --run >> /var/log/cron.log 2>&1

监控和通知

1 添加邮件通知功能

<?php
namespace CronSystem;
class Notification
{
    private $emailConfig;
    public function __construct(array $config)
    {
        $this->emailConfig = $config;
    }
    public function sendTaskFailureNotification(Task $task, string $error): void
    {
        $subject = "Task Failed: {$task->getName()}";
        $message = "Task '{$task->getName()}' failed at " . date('Y-m-d H:i:s');
        $message .= "\nError: $error";
        $message .= "\nCommand: " . $task->getCommand();
        $this->sendEmail($subject, $message);
    }
    private function sendEmail(string $subject, string $body): void
    {
        $headers = "From: " . $this->emailConfig['from'] . "\r\n";
        $headers .= "Content-type: text/html; charset=UTF-8\r\n";
        mail($this->emailConfig['to'], $subject, $body, $headers);
    }
}

性能优化建议

  1. 使用消息队列:对于大量任务,可以考虑使用 Redis 或 RabbitMQ 来管理任务队列
  2. 分布式执行:如果需要跨服务器执行,可以集成 Redis 作为分布式锁
  3. 监控警报:添加 Prometheus + Grafana 监控
  4. 数据库存储:将任务状态存储到 MySQL 以支持历史查询
  5. Web管理界面:开发一个简单的 PHP Web 界面来管理任务

安全考虑

  1. 权限控制:任务以最小权限运行
  2. 命令白名单:只允许执行预先定义的命令
  3. 超时处理:所有任务必须有明确的超时
  4. 资源限制:设置每个任务的内存和执行时间限制
  5. 审计日志:记录所有操作和更改

这个系统提供了完整的任务调度功能,包括并发执行、超时处理、重试机制、日志记录等核心功能,可以根据实际需求进行扩展。

抱歉,评论功能暂时关闭!