php邮件队列必须剥离web请求生命周期,否则即使使用redis,同步调用mail()或phpmailer仍会阻塞页面;根本原因是系统sendmail/smtp耗时且php web sapi同步执行,导致http连接挂起。

PHP邮件队列不是“加个缓存就能快”,而是必须把发送逻辑从Web请求生命周期里彻底剥离——否则哪怕用了Redis,只要还在$_POST里调用send(),用户就还在等。
为什么 mail() 或 PHPMailer 直接调用会卡住页面
因为mail()底层依赖系统sendmail或SMTP连接,一次调用可能耗时几百毫秒到几秒(DNS解析、TLS握手、远程服务器排队、反垃圾策略延迟)。而PHP的Web SAPI(如Apache mod_php、FPM)默认同步执行,没有返回Response前,整个HTTP连接就挂着。
- 现象:注册后页面白屏3秒、AJAX请求超时、Nginx报
upstream timed out - 本质:你不是在“发邮件”,是在“等别人帮你发”,还让所有用户陪你等
- 错觉:以为加个
ignore_user_abort(true)就能解决——它只让脚本后台跑,但FPM worker仍被占着,无法处理新请求
Redis List 做队列时最容易丢任务的三个操作
用LPUSH + BLPOP看似简单,但没处理好就会静默失败:
- 消费者进程崩溃后,
BLPOP取出的任务没来得及标记为“处理中”,直接消失——Redis无ACK机制 - 往
mail_queue推任务时没做serialize()校验,含resource或Closure的对象进队列,出队时unserialize()直接报Notice: unserialize(): Error at offset - 没加
attempts计数字段,任务反复失败重入,又没死信归档,最终塞满Redis内存
补救做法:LPUSH前先json_encode()并验证;消费者拿到任务后立刻UPDATE email_queue SET status = 1, attempts = attempts + 1 WHERE id = ? AND status = 0(带条件更新防重复消费)。
PHPMailer 复用 SMTP 连接的关键配置项
批量发100封邮件,建立100次SMTP连接 vs 复用1次,耗时差3倍以上。关键不是“设了SMTPKeepAlive = true”,而是怎么用:
-
$mail->SMTPAuth = true必须开启,否则SmtpConnect()不走认证缓存路径 - 首次
$mail->send()前手动调用$mail->SmtpConnect(),避免第一次自动连+认证的隐式开销 - 每封邮件发完不要
$mail->clearAddresses(),改用$mail->addAddress($to)动态追加,再$mail->send(),最后$mail->reset() - 连接空闲超时由SMTP服务端控制(如Gmail是30秒),PHPMailer不会主动保活,所以单次Worker处理不宜跨分钟级间隔
数据库队列表设计里最常被忽略的索引和锁
如果坚持用MySQL存队列(比如已有系统难上Redis),光靠status = 0查询会越来越慢:
- 必须给
(status, created_at)建联合索引,否则SELECT ... WHERE status = 0 ORDER BY created_at LIMIT 50全表扫描 - 更新时不能只
UPDATE SET status = 1,要WHERE status = 0 LIMIT 1 FOR UPDATE,否则并发Worker可能抢同一行,导致重复发送 -
status字段必须是TINYINT,别用VARCHAR('pending')——比较效率差一个数量级,且无法利用整型索引排序
真正卡住的往往不是发送本身,而是任务分发时的行锁争用或索引失效。上线前用EXPLAIN看一遍队列查询语句,比调优mail()参数实在得多。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











