不能直接在控制器调用Mail::send(),因同步发送会阻塞响应,导致超时、积压与雪崩;应封装为可序列化的Job类,通过队列异步执行,并配置超时、重试及监控。

为什么不能直接在控制器里调用 think\facade\Mail::send()
因为同步发邮件会卡住请求响应,网络抖动、SMTP超时、DNS解析失败都会让接口等上几秒甚至十几秒。用户点个提交按钮,转圈超过3秒基本就刷新重试了——这还只是单封邮件;并发高一点,PHP进程全堵在stream_socket_client上,队列积压、超时雪崩。
真实场景里,注册欢迎信、密码重置、订单通知这些都得“发出去就行”,不关心是否立刻送达,但必须保证不丢、可重试、能监控。
- 别在
__construct()或index()里写Mail::send(),哪怕加了try/catch也拦不住阻塞 - ThinkPHP 6.0+ 的
think-queue默认只支持job类,而think\facade\Mail是静态门面,不能直接序列化进队列 - 如果你用
dispatchNow()测试,看着像“异步”,其实还是当前进程同步执行,根本没走队列中间件
如何让 Mail 发送逻辑变成可入队的 Job 类
核心不是改邮件配置,而是把“发什么内容、发给谁、用哪个模板”这些信息提取成轻量数据,交给队列 Worker 去重建上下文再发。
不要继承think\queue\Job后硬塞Mail实例——序列化时会报__sleep(): Unable to serialize closure,因为门面底层绑了闭包和容器引用。
立即学习“PHP免费学习笔记(深入)”;
- 新建
app\job\SendMailJob.php,构造函数只接收$to、$subject、$template、$data等数组或字符串参数 - 在
handle()里重新获取think\facade\Mail,而不是传入或保存实例 - 模板路径统一用相对路径(如
email/welcome),避免Worker环境里view_path不一致导致View not found - 记得在
handle()开头加if (is_null($this->container)) { $this->container = app(); },防止某些版本下容器未自动注入
示例:
public function handle()
{
$mail = \think\facade\Mail::instance('default');
$mail->to($this->to)
->subject($this->subject)
->view($this->template, $this->data)
->send();
}
配置 SMTP 连接池与失败重试的关键参数
队列不是万能的缓冲垫,如果 SMTP 配置不当,失败任务会越堆越多,最后填满 Redis 或数据库表,连queue:work都起不来。
ThinkPHP 默认的mail.php配置没有连接超时控制,而 PHP 的fsockopen默认无限等待,一个卡死的连接就能拖垮整个 Worker 进程。
- 必须设置
'timeout' => 10(单位秒),同时在'smtp_host'后面补上端口,比如smtp.qq.com:587,否则stream_context_create可能忽略超时 -
'retry_after' => 600要大于单次发送最大耗时(含DNS+TLS握手+认证),否则任务还没发完就被标记为失败并重入队列 - Redis 驱动下,
'expire' => 3600建议设为retry_after * 3,避免任务还在跑,Redis key 却被删了,导致重复投递 - 用
php think queue:work --delay=3 --max-jobs=100 --memory=128启动,--delay防高频失败打爆邮箱服务商限流
怎么确认某封邮件真的进了队列、又确实发出去了
光看queue:work输出“Processed”不保险,得从三个层面交叉验证:数据落库、日志留痕、第三方回执。
最容易漏的是日志级别——默认queue通道日志只记info,而 SMTP 认证失败、5xx 响应这类错误只打error,结果你啥都没看到,还以为发成功了。
- 在
SendMailJob::handle()末尾加Log::channel('mail')->info('sent', ['to' => $this->to, 'job_id' => $this->job->getJobId()]); - 数据库驱动下,检查
think_queue表的failed_at字段是否为空;Redis 驱动则用redis-cli keys "queue:failed:*"扫失败队列 - QQ 邮箱、163 邮箱可在发信域名配置
DKIM和SPF,配合postmaster@xxx.com查退信日志,比看 PHP 日志更准 - 别依赖
$mail->send()返回true就认为成功——它只表示“SMTP 对话完成”,不校验对方是否真正收件入库
队列化的本质不是“让代码跑得更慢”,而是把不可控的外部依赖(网络、远程服务)从主流程里剥离开。一旦 SMTP 不稳定,你改的不该是重试次数,而是先确保每封邮件都有唯一message_id、可溯源、可人工干预补发。



















