直接用 Mail::queue() 或 Mail::later() 可跳过手动写 Job 类,但同步发送 Mail::send() 会阻塞 HTTP 请求导致超时;真正异步需设 QUEUE_CONNECTION=redis/database 并启动 worker 进程。

直接用 Mail::queue() 或 Mail::later() 就能跳过手动写 Job 类,这是最简路径;但若需精细控制重试、队列名、连接或失败回调,必须走自定义 Job 类。
为什么不能只靠 Mail::send()?
同步发送会卡住 HTTP 请求,用户看到白屏或超时。Laravel 默认的 sync 队列驱动其实不异步——它只是把 send() 包了一层,仍在线程里执行。真正异步必须满足两个条件:队列驱动设为 redis / database 等非 sync 类型,且任务实际进入后台 worker 进程处理。
常见错误现象:
-
QUEUE_CONNECTION=sync时调Mail::queue(),日志里显示“dispatched”,但邮件还是同步发、响应没变快 - 改了
.env但没运行php artisan config:clear,配置未生效 - 本地开发用
database驱动却忘了跑php artisan queue:table && php artisan migrate,队列写不进表
Mail::queue() 和 Mail::later() 怎么选?
两者都省去 Job 类,但语义和底层行为不同:
-
Mail::queue(new WelcomeMail($user))→ 立即入队,由queue:work拉取后立刻执行 -
Mail::later(now()->addMinutes(5), new WelcomeMail($user))→ 入队时设置available_at时间戳,worker 只在该时间点之后才拉取
注意:later() 在 database 驱动下依赖轮询(有延迟),在 redis 驱动下更准(利用 ZSET 排序)。若需精确到秒级调度,别用 database。
参数差异:
- 两者都支持链式
to()/cc()/bcc(),但不支持onConnection()或onQueue()—— 想指定连接或队列名?只能回退到 Job 类 -
later()第一个参数必须是DateTimeInterface或Carbon实例,传整数秒会报错
自定义 Job 类绕不开的三个坑
当你要做失败重试、分队列、跨连接或加日志埋点时,Job 类是唯一选择。但容易栽在这三处:
-
构造函数里别序列化 Eloquent 模型:写
$this->user = $user看似方便,但模型含大量关联和原始 SQL 查询器,序列化失败或反序列化后丢失状态。正确做法是只存 ID:$this->userId = $user->id,handle()里再查 -
retry_after要大于邮件发送耗时:比如 SMTP 超时设为 60 秒,但config/queue.php里redis.retry_after是 90,那网络抖动时任务可能被重复执行两次。建议设为超时值 + 10~20 秒余量 -
Job 类里别直接调
Mail::send():必须用Mail::to(...)->send()链式调用,否则门面无法自动注入收件人上下文,会抛InvalidArgumentException
Redis 驱动下必须确认的两件事
用 Redis 是性能最优解,但配置不对等于白搭:
-
REDIS_QUEUE环境变量默认是default,但Mail::queue()默认发到mails队列。结果就是你启动php artisan queue:work --queue=default,邮件全积压在mails里不动。解决方法:要么改.env加REDIS_QUEUE=mails,要么启动时显式指定--queue=mails - Redis 连接池默认是单连接,高并发下易阻塞。在
config/database.php的redis配置里加'options' => ['prefix' => 'laravel_database_']并确保predis/predis版本 ≥ 1.1,否则连接复用失效
最后提醒一句:queue:work 进程一旦启动,就不会自动加载新的环境变量或代码变更。上线后改了邮件模板或 Job 逻辑,必须重启 worker,否则旧代码还在跑。这点比缓存更隐蔽,也更容易被忽略。


















