本文目录导读:

我来为您详细介绍PHP分布式定时任务调度的完整方案。
1 核心问题
- 单机定时任务存在单点故障
- 任务重复执行问题
- 任务分发与负载均衡
- 任务状态监控与管理
2 主流解决方案
graph TB
A[分布式定时任务方案] --> B[基于消息队列]
A --> C[基于Redis]
A --> D[开源框架]
A --> E[云服务]
B --> B1[RabbitMQ延迟队列]
B --> B2[Kafka定时消息]
C --> C1[Redis ZSet]
C --> C2[Redis Keyspace]
D --> D1[XXL-JOB]
D --> D2[Elastic-Job]
D --> D3[Swoole框架]
E --> E1[阿里云SchedulerX]
E --> E2[腾讯云TCT]
完整实现方案
1 基于Redis的分布式调度器
<?php
/**
* Redis分布式任务调度器
*/
class RedisScheduler
{
private $redis;
private $taskKey = 'scheduler:tasks';
private $taskPrefix = 'scheduler:task:';
private $lockPrefix = 'scheduler:lock:';
private $nodeId;
public function __construct($redisConfig, $nodeId)
{
$this->redis = new Redis();
$this->redis->connect($redisConfig['host'], $redisConfig['port']);
$this->redis->auth($redisConfig['password'] ?? '');
$this->redis->select($redisConfig['database'] ?? 0);
$this->nodeId = $nodeId;
}
/**
* 添加定时任务
*/
public function addTask($taskName, $cronExpr, $callback, $params = [])
{
$taskId = md5($taskName . uniqid());
$task = [
'id' => $taskId,
'name' => $taskName,
'cron' => $cronExpr,
'callback' => $callback,
'params' => $params,
'status' => 'active',
'created_at' => time()
];
// 存储任务详情
$this->redis->hMset($this->taskPrefix . $taskId, $task);
// 计算下次执行时间
$nextRunTime = $this->getNextRunTime($cronExpr);
// 加入有序集合,score为下次执行时间
$this->redis->zAdd($this->taskKey, $nextRunTime, $taskId);
return $taskId;
}
/**
* 调度器主循环
*/
public function run()
{
echo "调度器节点 {$this->nodeId} 启动\n";
while (true) {
$this->processDueTasks();
usleep(100000); // 100ms间隔
}
}
/**
* 处理到期任务
*/
private function processDueTasks()
{
$now = microtime(true) * 1000;
// 获取所有到期的任务ID
$dueTasks = $this->redis->zRangeByScore($this->taskKey, 0, $now);
foreach ($dueTasks as $taskId) {
// 尝试获取分布式锁,避免重复执行
$lockKey = $this->lockPrefix . $taskId;
if ($this->acquireLock($lockKey, $taskId)) {
try {
// 从有序集合移除
$this->redis->zRem($this->taskKey, $taskId);
// 执行任务
$this->executeTask($taskId);
// 计算下次执行时间
$taskInfo = $this->redis->hGetAll($this->taskPrefix . $taskId);
if ($taskInfo && $taskInfo['status'] === 'active') {
$nextRunTime = $this->getNextRunTime($taskInfo['cron']);
$this->redis->zAdd($this->taskKey, $nextRunTime, $taskId);
}
} finally {
// 释放锁
$this->releaseLock($lockKey, $taskId);
}
}
}
}
/**
* 获取分布式锁
*/
private function acquireLock($lockKey, $taskId, $timeout = 10)
{
$token = $this->nodeId . ':' . uniqid();
$result = $this->redis->set($lockKey, $token, ['NX', 'EX' => $timeout]);
if ($result) {
// 设置锁的值,用于确认
$this->redis->set($lockKey . ':token', $token, ['EX' => $timeout]);
return true;
}
return false;
}
/**
* 释放锁
*/
private function releaseLock($lockKey, $taskId)
{
$token = $this->redis->get($lockKey . ':token');
$luaScript = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
$this->redis->eval($luaScript, [$lockKey, $token], 1);
$this->redis->del($lockKey . ':token');
}
/**
* 计算Cron表达式下次执行时间
*/
private function getNextRunTime($cronExpr)
{
$cron = CronExpression::factory($cronExpr);
$nextRun = $cron->getNextRunDate();
return $nextRun->getTimestamp() * 1000;
}
/**
* 执行任务(异步)
*/
private function executeTask($taskId)
{
$taskInfo = $this->redis->hGetAll($this->taskPrefix . $taskId);
// 发送到消息队列异步执行
$this->sendToQueue($taskId, $taskInfo);
}
private function sendToQueue($taskId, $taskInfo)
{
// 这里可以对接 RabbitMQ/Kafka 等
$event = new TaskEvent([
'task_id' => $taskId,
'params' => $taskInfo['params'] ?? [],
'callback' => $taskInfo['callback'] ?? ''
]);
// 使用异步进程或消息队列
event($event);
}
}
2 Cron表达式解析器
<?php
/**
* Cron表达式解析器
*/
class CronExpression
{
private $minutes = [];
private $hours = [];
private $days = [];
private $months = [];
private $weekdays = [];
public static function factory($expression)
{
$instance = new self($expression);
return $instance;
}
public function __construct($expression)
{
$parts = preg_split('/\s+/', trim($expression));
if (count($parts) !== 5) {
throw new InvalidArgumentException('Cron表达式格式错误');
}
list($minute, $hour, $day, $month, $weekday) = $parts;
$this->minutes = $this->parseField($minute, 0, 59);
$this->hours = $this->parseField($hour, 0, 23);
$this->days = $this->parseField($day, 1, 31);
$this->months = $this->parseField($month, 1, 12);
$this->weekdays = $this->parseField($weekday, 0, 6);
}
private function parseField($field, $min, $max)
{
$values = [];
if ($field === '*') {
return range($min, $max);
}
// 处理逗号分隔
foreach (explode(',', $field) as $part) {
if (strpos($part, '/') !== false) {
list($range, $step) = explode('/', $part);
$start = $range === '*' ? $min : intval($range);
for ($i = $start; $i <= $max; $i += intval($step)) {
$values[] = $i;
}
} elseif (strpos($part, '-') !== false) {
list($start, $end) = explode('-', $part);
$values = array_merge($values, range(intval($start), intval($end)));
} else {
$values[] = intval($part);
}
}
sort($values);
return array_unique($values);
}
public function getNextRunDate()
{
$current = new DateTime();
$current->setTime($current->format('H'), $current->format('i'), 0);
for ($i = 0; $i < 1440; $i++) { // 24小时搜索
$date = clone $current;
$date->modify("+{$i} minutes");
if ($this->matches($date)) {
return $date;
}
}
throw new RuntimeException('找不到下次执行时间');
}
private function matches(DateTime $date)
{
return in_array(intval($date->format('i')), $this->minutes) &&
in_array(intval($date->format('H')), $this->hours) &&
in_array(intval($date->format('d')), $this->days) &&
in_array(intval($date->format('m')), $this->months) &&
in_array(intval($date->format('w')), $this->weekdays);
}
}
3 分布式任务执行器
<?php
/**
* 分布式任务Worker
*/
class TaskWorker
{
private $queue;
private $processor;
private $logger;
private $maxRetries = 3;
public function __construct($queue, $processor, $logger)
{
$this->queue = $queue;
$this->processor = $processor;
$this->logger = $logger;
}
/**
* 启动Worker
*/
public function start($workerCount = 4)
{
echo "启动 {$workerCount} 个Worker进程\n";
for ($i = 0; $i < $workerCount; $i++) {
$pid = pcntl_fork();
if ($pid === -1) {
throw new RuntimeException('进程创建失败');
} elseif ($pid === 0) {
// 子进程处理任务
$this->processLoop($i);
exit(0);
}
}
// 主进程等待
while (true) {
$status = 0;
pcntl_wait($status);
}
}
/**
* 任务处理循环
*/
private function processLoop($workerId)
{
echo "Worker {$workerId} 启动\n";
while (true) {
try {
// 从队列获取任务
$task = $this->queue->pop('task_queue', 5);
if ($task) {
$this->processTask($task, $workerId);
}
usleep(100000); // 100ms
} catch (Exception $e) {
$this->logger->error('任务处理异常: ' . $e->getMessage());
usleep(1000000); // 1s
}
}
}
/**
* 处理单个任务
*/
private function processTask($task, $workerId)
{
$taskId = $task['task_id'];
$callback = $task['callback'];
$params = $task['params'];
echo "Worker {$workerId} 处理任务: {$taskId}\n";
for ($attempt = 1; $attempt <= $this->maxRetries; $attempt++) {
try {
// 执行任务
$result = $this->executeCallback($callback, $params);
// 记录执行日志
$this->logger->info("任务 {$taskId} 执行成功", [
'worker' => $workerId,
'attempt' => $attempt,
'result' => $result
]);
return true;
} catch (Exception $e) {
$this->logger->error("任务 {$taskId} 执行失败", [
'worker' => $workerId,
'attempt' => $attempt,
'error' => $e->getMessage()
]);
if ($attempt < $this->maxRetries) {
// 退避重试
sleep($attempt * 5);
}
}
}
// 任务最终失败,发送告警
$this->sendAlert($taskId, "任务执行失败超过最大重试次数");
return false;
}
/**
* 执行回调函数
*/
private function executeCallback($callback, $params)
{
if (is_callable($callback)) {
return call_user_func($callback, $params);
}
if (strpos($callback, '@') !== false) {
list($class, $method) = explode('@', $callback);
$instance = new $class();
return $instance->$method($params);
}
throw new RuntimeException("无效的回调方式");
}
}
4 使用XXL-JOB方案
<?php
/**
* XXL-JOB PHP客户端
*/
class XxlJobHandler
{
private $token;
private $adminAddress;
private $executorId;
/**
* 配置处理器
*/
public function registerHandler($jobId, $handler)
{
$handlerMap[$jobId] = $handler;
return $this;
}
/**
* 执行Job处理逻辑
*/
public function execute($jobId, $jobParam)
{
$jobInfo = $this->getJobInfo($jobId);
switch ($jobInfo['type']) {
case 'bean':
return $this->executeBeanJob($jobInfo, $jobParam);
case 'script':
return $this->executeScriptJob($jobInfo, $jobParam);
case 'http':
return $this->executeHttpJob($jobInfo, $jobParam);
default:
throw new RuntimeException("未知任务类型");
}
}
/**
* Bean类型任务
*/
private function executeBeanJob($jobInfo, $jobParam)
{
$handler = $this->handlerMap[$jobInfo['handler']] ?? null;
if (!$handler) {
throw new RuntimeException("处理器未注册");
}
return $handler($jobParam);
}
/**
* HTTP回调方式
*/
public function httpCallback($url, $params, $timeout = 30)
{
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json'
]
]);
$result = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new RuntimeException("请求失败:{$error}");
}
return json_decode($result, true);
}
}
5 监控与管理
<?php
/**
* 任务监控与管理
*/
class TaskMonitor
{
private $redis;
private $logger;
/**
* 记录任务执行统计
*/
public function recordExecution($taskId, $status, $data = [])
{
$key = "task:stats:{$taskId}";
$this->redis->hIncrBy($key, 'total', 1);
$this->redis->hIncrBy($key, $status, 1);
$this->redis->hSet($key, 'last_run', time());
if ($status == 'success') {
$this->redis->hSet($key, 'last_success', time());
} else {
$this->redis->hSet($key, 'last_error', json_encode($data));
}
// 更新每日统计
$dailyKey = "task:daily:{$taskId}:" . date('Y-m-d');
$this->redis->hIncrBy($dailyKey, 'total', 1);
$this->redis->hIncrBy($dailyKey, $status, 1);
}
/**
* 发送告警
*/
public function sendAlert($taskId, $message, $level = 'warning')
{
$alert = [
'task_id' => $taskId,
'message' => $message,
'level' => $level,
'time' => date('Y-m-d H:i:s')
];
// 优先使用钉钉/企业微信
if (class_exists('DingTalk')) {
DingTalk::send($alert);
}
// 邮件告警备用
$this->logger->alert(json_encode($alert));
}
/**
* 获取任务健康状态
*/
public function getTaskHealth()
{
$tasks = $this->redis->keys('task:stats:*');
$health = [];
foreach ($tasks as $task) {
$id = str_replace('task:stats:', '', $task);
$stats = $this->redis->hGetAll($task);
$lastRun = $stats['last_run'] ?? 0;
$age = time() - $lastRun;
$health[$id] = [
'total' => $stats['total'] ?? 0,
'success' => $stats['success'] ?? 0,
'failed' => $stats['failed'] ?? 0,
'last_run' => $lastRun,
'last_success' => $stats['last_success'] ?? 0,
'status' => $age > 3600 ? 'warning' : 'ok'
];
}
return $health;
}
}
6 启动脚本
<?php
/**
* 分布式调度系统启动入口
*/
class SchedulerApplication
{
private $config;
public function __construct($config)
{
$this->config = $config;
$this->init();
}
private function init()
{
date_default_timezone_set('Asia/Shanghai');
require_once 'vendor/autoload.php';
}
/**
* 启动调度器
*/
public function runScheduler()
{
$nodeId = gethostname() . ':' . getmypid();
$scheduler = new RedisScheduler($this->config['redis'], $nodeId);
// 注册任务
$this->registerTasks($scheduler);
// 启动调度
$scheduler->run();
}
/**
* 启动Worker
*/
public function runWorker()
{
$worker = new TaskWorker(
new QueueClient($this->config['queue']),
new TaskProcessor(),
new Logger($this->config['log'])
);
$worker->start($this->config['worker_count'] ?? 4);
}
/**
* 注册系统任务
*/
private function registerTasks($scheduler)
{
// 数据清理任务
$scheduler->addTask(
'cleanup_logs',
'0 2 * * *',
function() {
LogCleaner::cleanup(7);
}
);
// 报表生成任务
$scheduler->addTask(
'generate_report',
'0 6 * * *',
'ReportGenerator@generateDaily',
['format' => 'pdf', 'send_email' => true]
);
// 缓存刷新任务
$scheduler->addTask(
'refresh_cache',
'*/10 * * * *',
'CacheRefresher@refresh',
[]
);
}
}
部署架构
# Process Manager 配置 ; /etc/supervisor/conf.d/scheduler.conf [program:php_scheduler] command=php /var/www/scheduler/bin/scheduler.php directory=/var/www/scheduler user=www-data numprocs=1 autostart=true autorestart=true startsecs=10 stdout_logfile=/var/log/scheduler/scheduler.log [program:php_worker] command=php /var/www/scheduler/bin/worker.php directory=/var/www/scheduler user=www-data numprocs=4 autostart=true autorestart=true startsecs=10 stdout_logfile=/var/log/scheduler/worker.log
最佳实践
1 任务设计原则
- 幂等性:任务必须支持重复执行
- 超时控制:设置合理的执行超时
- 失败重试:带退避策略的重试机制
- 任务分级:按优先级分配资源
2 性能优化
- 使用消息队列异步处理
- 批量处理提高效率
- 合理设置Worker数量
- 避免长任务阻塞
3 监控告警
- 任务失败实时告警
- 任务执行延迟监控
- 资源使用率监控
- 分钟级心跳检测
其他开源方案推荐
- XXL-JOB: 功能丰富的分布式任务平台
- Elastic-Job: 基于Zookeeper的分布式调度
- Swoole Framework: 高性能PHP框架,适合长进程任务
- Workerman: 高性能PHP Socket框架
- 阿里云SchedulerX: 云端托管方案
这种分布式架构可以支持大规模的任务调度,确保高可用和可扩展性,选择方案时需要根据项目需求、团队技术栈和运维能力综合考虑。