延迟任务应使用队列系统dispatch()->delay()实现,而非调度器schedule();需配置支持延迟的队列驱动、job实现shouldqueue接口、设置tries与backoff,并确保queue:work常驻运行。

延迟任务在 Laravel 里不是调度器(schedule)的职责,而是队列系统(dispatch()->delay())的功能。别往 App\Console\Kernel 的 schedule() 方法里硬塞“延迟执行”逻辑——它只管“什么时候该触发”,不负责“触发后等几秒再干”。真要延迟,得走队列。
延迟任务必须用队列驱动,不能靠 schedule + at() 模拟
有人试图这样写:
$schedule->command('notify:send')->daily()->at('14:30');
以为这就是“延迟到下午2:30发通知”,其实不是:这是每天固定时间触发一次命令,和“用户注册后 5 分钟发欢迎邮件”这种动态延迟完全无关。
-
schedule是静态时间表,所有任务的触发时间在部署时就确定了 - 真正需要延迟的场景(比如订单超时取消、验证码过期提醒),必须由业务代码在运行时决定延迟多久,靠
dispatch()->delay()实现 - 如果队列驱动没配好(比如
QUEUE_CONNECTION=sync),delay()会直接忽略,任务立刻执行
正确用法:dispatch()->delay() + 可失败任务
延迟任务本质是把 Job 推进队列,并设置一个未来时间戳。Laravel 会等那个时间到了才从队列取出执行。
- 确保
.env中已设QUEUE_CONNECTION=database(或redis、beanstalkd等支持延迟的驱动) - Job 类需实现
ShouldQueue接口,且不要在handle()里写阻塞操作 - 延迟单位是
Seconds,不是字符串:now()->addMinutes(5)或5 * 60 - 务必加
tries和backoff防止失败卡死队列:public $tries = 3; public $backoff = 60;
示例:
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
常见错误:用 schedule 模拟延迟导致任务堆积
比如想实现“用户下单后 30 分钟未支付就关单”,错误做法是:
$schedule->command('order:close')->everyMinute(); // 然后在命令里查所有 30 分钟前创建的未支付订单
这会造成严重问题:
- 每分钟全表扫描订单表,IO 和 CPU 压力随数据量线性增长
- 关单逻辑无法精准控制在“下单后第 30 分钟整”,可能偏差几十秒甚至更久
- 如果命令执行慢,下次调度又来,多个实例并发处理同一订单
正确解法是:下单成功时立刻 dispatch 一个带 delay 的 Job,让队列系统精确控制触发时机。
生产环境必须确认队列监听器在运行
dispatch()->delay() 只是把任务写进数据库/Redis,真正执行靠 php artisan queue:work 或 supervisor 管理的常驻进程。这点极易被忽略:
- 本地开发常用
queue:work --once测试,但生产必须用--daemon模式并配合进程管理工具 - Redis 驱动下,延迟任务依赖
BRPOPLPUSH或ZSET,若 Redis 内存不足或 key 过期策略激进,延迟可能失效 - database 驱动下,
jobs表的available_at字段必须被queue:work正确读取,否则延迟任务永远不执行
检查是否生效最简单的方法:执行 php artisan queue:work --once 后立刻看日志,确认延迟任务是否出现在 “Processing…” 日志行里。











