laravel调度需同时满足三要素:显式声明频率(如->dailyat()或->cron())、系统级每分钟执行schedule:run、时区与缓存等环境配置正确;缺一任务即静默失效。

直接说结论:Laravel 调度频率不是靠写 Cron 表达式“猜时间”,而是用链式方法(如 ->dailyAt('09:00'))或 ->cron('0 9 * * *') 显式声明,且必须配合系统级每分钟执行的 schedule:run 才能生效。漏掉任意一环,任务就静默不跑。
为什么 ->hourly() 不等于“整点触发”
默认情况下,->hourly() 表示“每小时的第 0 分钟触发”,但实际执行时刻取决于 schedule:run 被系统 cron 调起的时间点。如果服务器 cron 每分钟执行一次,而 Laravel 在 14:00:23 检查到该任务应运行,则它会在 14:00:23 执行,不是严格卡在 14:00:00。
-
->hourly()等价于->cron('0 * * * *'),只保证分钟位为 0,不控制秒级精度 - 若需严格整点(如数据库备份必须在 02:00:00 开始),得加
->withoutOverlapping()防止前次残留干扰,再配合日志确认启动时间 - 本地开发时,
php artisan schedule:run手动执行会立即触发所有“已到时间”的任务,但这不反映线上真实节奏——别靠它验证定时逻辑
->dailyAt('09:30') 时区坑在哪
这个方法默认用服务器本地时区,但 Laravel 应用时区由 config/app.php 中的 'timezone' 决定,两者不一致就会错乱。比如服务器在 UTC,应用设为 'Asia/Shanghai',->dailyAt('09:30') 实际按 UTC+8 解析,但 cron 触发仍按服务器 UTC 时间判断是否“到了 09:30”,结果任务永远差 8 小时。
- 统一方案:所有调度显式加
->timezone('Asia/Shanghai'),例如$schedule->command('notify:today')->dailyAt('09:30')->timezone('Asia/Shanghai') - 数据库字段(如
event_date)也必须和该时区一致,否则whereBetween()查不到数据 -
now()->startOfHour()这类 Carbon 方法自动受app.timezone影响,但系统 cron 不认这个配置——所以不能只依赖 PHP 层时区
动态时间调度别用 ->when() 做秒级判断
有人想在 schedule() 里遍历数据库、用 ->when() 判断「当前秒是否等于 event_date」,这完全违背 Laravel 调度机制:调度器每分钟只运行一次,->when() 回调也只在这一次中执行。如果事件时间是 15:25:47,而调度器在 15:25:00 和 15:26:00 各检查一次,中间那 47 秒就被跳过了。
- 正确做法是每分钟查一次“本分钟区间”,用
whereBetween('event_date', [now()->startOfMinute(), now()->endOfMinute()]) - 不要在
->call()闭包里做重查询或长耗时操作,改用dispatch(new HandleEvent($event))丢进队列 - 避免在
schedule()中调用DB::transaction()或锁表,调度进程是短命的,事务不会延续到下一次
withoutOverlapping() 的锁机制怎么失效
这个方法靠 Laravel 缓存(默认是 file 或 redis)存一个带过期时间的键来实现互斥。但如果缓存驱动不可用、权限不对,或者多个服务器共用同一套调度配置但没配 onOneServer(),锁就形同虚设。
- 检查
storage/framework/cache/data/目录权限,确保运行 cron 的用户(如www-data)有读写权 - Redis 缓存需确认
cache.store配置正确,且redis连接未被防火墙拦截 - 多实例部署时,必须加
->onOneServer(),否则每台机器都尝试抢锁,最终可能全跑或全不跑 - 锁默认有效期 24 小时,如果任务真卡死超过一天,后续调度会被永久阻塞——建议配合超时监控
真正难的不是写对 ->weeklyOn(3, '14:00'),而是让这行代码在凌晨三点的生产服务器上,准时、不重复、不丢失地跑完。时区、缓存、权限、日志路径、环境变量——每个环节都可能无声断掉那根触发线。











