PHP创建Cron执行脚本的正确方式及现有问题解决
PHP定时任务(Cron)的正确实现方案
针对你遇到的文件锁卡住、脚本超时,以及双重Cron触发的问题,以下是具体的修复和优化方案:
一、文件锁问题的修复
当前代码的锁处理存在漏洞,容易导致锁无法释放:
- 文件打开模式错误:使用
r+模式需要文件提前存在,改为c+模式可自动创建文件,避免找不到文件的错误; - 锁释放不严谨:缺少异常捕获,脚本如果意外退出(比如致命错误)会导致锁一直持有;另外
flock($fp, LOCK_UN)末尾少了分号,语法错误会导致锁无法释放; - 无意义等待:
sleep(2)完全没必要,只会增加脚本执行时间。
改进后的锁处理逻辑:
$lockFile = __DIR__ . '/perform_cronjobs_lock.txt'; $fp = fopen($lockFile, 'c+'); if (!$fp) { die("无法打开锁文件"); } // 使用try-finally确保锁一定会释放 try { if (!flock($fp, LOCK_EX | LOCK_NB, $blocked)) { if ($blocked) { die("定时任务正在执行中"); } else { die("获取锁失败"); } } // 写入当前PID和时间,方便排查卡住的情况 ftruncate($fp, 0); fwrite($fp, "PID: " . getmypid() . " | 执行时间: " . date('Y-m-d H:i:s')); fflush($fp); // 这里放任务执行逻辑 } finally { flock($fp, LOCK_UN); fclose($fp); }
二、超时问题的解决
- 避免依赖set_time_limit:如果是通过CLI执行PHP脚本(本地Cron推荐方式),
set_time_limit默认是无限制的;如果是外部Cron通过HTTP访问,Web服务器(比如Nginx/Apache)也会有自己的超时设置,需要同时调整; - 任务拆分:不要单次执行过多任务,当前的
$limit -= 0.1逻辑很混乱,改为按固定数量拆分,比如每次最多处理10个普通任务、50个RSS任务,剩下的留给下一次Cron执行; - 优化任务逻辑:检查
perform_cronjob方法里的数据库操作,给查询字段加索引,避免全表扫描;减少不必要的IO操作,比如合并数据库请求。
三、Cron管理规范
- 停止重复触发源:立即停用外部Cron或本地系统Cron中的一个,避免两个源同时触发导致锁冲突;
- 优先使用CLI模式执行:本地Cron直接调用PHP CLI执行脚本,比如:
这种方式比HTTP访问更稳定,没有Web服务器的超时限制,也能直接输出日志;* * * * * /usr/bin/php /path/to/perform_cronjobs.php >> /path/to/cron_logs.log 2>&1 - 添加日志记录:在脚本中记录每个任务的执行情况,方便排查问题:
$logFile = __DIR__ . '/cron_logs.log'; function writeLog($msg) { global $logFile; file_put_contents($logFile, date('Y-m-d H:i:s') . " - " . $msg . "\n", FILE_APPEND); } // 任务执行后记录 writeLog("执行了RSS读取任务,剩余限制: {$limit}");
四、优化后的完整代码示例
<?php // CLI模式下无需设置时间限制,网页模式可根据需要调整 if (php_sapi_name() !== 'cli') { set_time_limit(120); } $lockFile = __DIR__ . '/perform_cronjobs_lock.txt'; $logFile = __DIR__ . '/cron_logs.log'; $fp = fopen($lockFile, 'c+'); if (!$fp) { writeLog("无法打开锁文件"); die("无法打开锁文件"); } function writeLog($msg) { global $logFile; file_put_contents($logFile, date('Y-m-d H:i:s') . " - " . $msg . "\n", FILE_APPEND); } try { if (!flock($fp, LOCK_EX | LOCK_NB, $blocked)) { if ($blocked) { writeLog("任务已在执行,跳过本次触发"); die("任务已在执行"); } else { writeLog("获取锁失败"); die("获取锁失败"); } } ftruncate($fp, 0); fwrite($fp, "PID: " . getmypid() . " | 执行时间: " . date('Y-m-d H:i:s')); fflush($fp); writeLog("开始执行定时任务"); $pageid = "perform_cronjobs.php"; include_once(__DIR__ . "/backend/include.php"); $currentMinute = (int)date("i"); if ($currentMinute === 30) { // 每小时执行的任务 writeLog("执行每小时任务"); } else { // 每分钟执行的任务 $list = cronjob::select_all_past_cronjob_classes(); $normalLimit = 10; // 普通任务每次最多执行10个 $rssLimit = 50; // RSS任务每次最多执行50个 foreach($list as $cronjob){ if ($cronjob->name === "manual_action") { continue; } if ($cronjob->name === "read_rss") { if ($rssLimit <= 0) { continue; } $cronjob->perform_cronjob(); $rssLimit--; writeLog("执行RSS读取任务,剩余配额: {$rssLimit}"); } else { if ($normalLimit <= 0) { continue; } $cronjob->perform_cronjob(); $normalLimit--; writeLog("执行任务: {$cronjob->name},剩余配额: {$normalLimit}"); } } } end_of_file($pageid); writeLog("定时任务执行完成"); } catch (Exception $e) { writeLog("任务执行出错: " . $e->getMessage()); throw $e; } finally { flock($fp, LOCK_UN); fclose($fp); } function end_of_file($pageid) { // 你的数据库操作逻辑 } ?>
内容的提问来源于stack exchange,提问作者erena
相关产品推荐
相关产品推荐

