laravel 的 schedule:run 必须由系统 cron 每分钟调用,因其本身仅为计划解析器,非守护进程;不配置 cron 则任务永不执行。

任务没跑,八成是 schedule:run 没被系统 cron 正确调用——不是 Laravel 写得不对,而是调度器本身不自动运行,它只等那一分钟一次的“敲门声”。
为什么 schedule:run 必须配系统 cron?
Laravel 的 Schedule 类只是个计划解析器,不是守护进程。它不会自己监听时间、也不会后台常驻。每次执行 php artisan schedule:run,框架才去 Kernel.php 里遍历所有注册任务,比对当前时间是否匹配其频率(比如 dailyAt('02:00')),再决定是否触发。
- 没有系统 cron,
schedule:run就永远不会被执行,任务永远“躺在计划表里” -
schedule:work是替代方案,但它依赖 Supervisor 或容器前台运行,不是开箱即用的默认路径 - Docker 环境下更要注意:基础镜像通常不带 cron 守护进程,
crontab -e可能根本不可用
schedule:run 的 cron 行怎么写才不出错?
最简配置是:* * * * * cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1,但生产环境容易栽在这几处:
- 路径必须用绝对路径,
cd后不能是相对路径(如./或~/myapp) - PHP 可执行文件建议写全路径,比如
/usr/bin/php,尤其在 cPanel、Plesk 或 Docker 中,php命令可能不可见或版本不对 - 日志重定向别丢掉:
>> /var/www/myapp/storage/logs/schedule.log 2>&1比>> /dev/null更利于排查(比如权限拒绝、.env 加载失败) - 确认 cron 运行用户有权限读
.env、写storage/、执行artisan——常见于用www-data跑 cron,但部署时又用root或deploy用户部署
command() 和 call() 到底该选哪个?
这不是风格问题,是执行上下文和健壮性的分水岭:
-
$schedule->command('backup:database --gzip'):走完整 Artisan 生命周期,支持参数、选项、事件(CommandStarting)、异常捕获、自动服务解析;适合涉及数据库、邮件、队列、文件操作的任务 -
$schedule->call(function () { cache()->increment('api_hits'); }):直接调闭包,无中间件、无命令生命周期、不触发事件;适合极轻量、纯内存/缓存操作,且你 100% 确保闭包内不依赖未初始化的服务(比如在AppServiceProvider::register()之前就调用了DB::connection()) - 闭包里调
dispatch(new SendEmailJob)很危险:Job 序列化后反序列化时,若类名或命名空间变更,会静默失败;而command()是字符串调用,更稳定
本地开发时任务总跳过?先查锁和时区
常见现象是手动跑 php artisan schedule:run 显示 “No scheduled commands are ready to run”,但明明到了设定时间。大概率卡在两个地方:
- 锁机制干扰:
withoutOverlapping()默认用文件锁,路径在storage/framework/cache/data/。Homestead/Valet/Docker 共享目录常因 NFS 缓存或挂载权限导致锁文件创建失败,结果调度器误判“任务已在运行”,直接跳过 - 时区错位:Laravel 默认用服务器时区,但
config/app.php的'timezone' => 'UTC'和服务器date输出不一致时,dailyAt('09:00')实际按 UTC 09:00 判定,而非你期望的东八区 09:00 - 验证方式:加
->withoutOverlapping(0)临时绕过锁;用php artisan schedule:list(Laravel ≥8.73)看“Next Due”时间是否符合预期时区
真正麻烦的从来不是写几行 $schedule->command(),而是让那条系统 cron 稳稳当当地每分钟敲一次门,并确保门后所有依赖(权限、路径、时区、锁存储)都就绪——漏掉任意一环,任务就停在计划表里,无声无息。











