webman中扛得住邮件群发的队列驱动是redis(启用delayed模式,基于zset)或amqp(如rabbitmq),二者均支持高可靠异步投递与重试语义,可稳定支撑1000+ qps邮件下发。

Webman 中用哪个队列驱动扛得住邮件群发?
直接说结论:别碰 file 驱动,也别默认用 redis 的 list + blpop 实现——它在高并发下容易丢任务、无法保证重试语义。真正能稳住 1000+ QPS 邮件下发的,是 redis 驱动配合 delayed 模式(基于 zset),或直接切到 amqp(如 RabbitMQ)。Webman 官方 webman/console 插件默认支持这两者,但配置稍有区别。
实操建议:
-
redis方案:确保 Redis 版本 ≥ 6.2(需要ZMSCORE支持),在config/queue.php中设'default' => 'redis',并启用'delayed' => true -
amqp方案:装webman/amqp插件,配置vhost和durablequeue,关键要设delivery_mode => 2(持久化消息),否则进程重启后队列空了 - 别在
app/command/SendMailCommand.php里直接Mail::send()—— 必须走dispatchNow()或dispatch()推进队列,否则压根没走异步
怎么让每封邮件都带独立模板和收件人,又不爆内存?
常见错误是把几万条 $users 数组全塞进一个 job 类的 $data 属性里,序列化后占几十 MB,Redis 内存飙升,还触发 maxmemory-policy 清理导致丢任务。
正确做法是「数据不动,逻辑分片」:
- 投递时只传关键标识:比如
['task_id' => 'mail_abc123', 'batch_no' => 5],而非用户列表 - Job 执行时再按
task_id查数据库或缓存拉取本批次的 100 条用户(用offset/limit或游标) - 模板渲染必须用
view()->fetch()而非view()->render(),前者返回字符串,后者可能触发响应输出逻辑 - 如果模板含动态图片,别用
file_get_contents()读本地路径——改成http://127.0.0.1:8787/static/logo.png,避免 worker 进程阻塞 IO
发信失败后怎么重试?为什么 retry_times 设了 3 次却只执行 1 次?
根本原因:Webman 的队列重试依赖于 job 抛出异常。如果你在 handle() 里写了 try/catch 吞掉所有异常,或者用 return false 代替抛错,框架就认为“执行成功”,不会重试。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
必须这样写:
public function handle()
{
try {
$this->sendViaSmtp();
} catch (\Exception $e) {
// 记日志
logger()->error('Mail send failed', ['task_id' => $this->task_id, 'error' => $e->getMessage()]);
// 必须 re-throw,不能 return 或 log 后静默
throw $e;
}
}
另外注意:retry_times 是全局配置,写在 config/queue.php 的 connections.redis.retry_times 下;单个 job 也可覆盖,调用 dispatch($job)->onConnection('redis')->delay(60)->tries(3)。
SMTP 连接池不够用,发着发着就卡住或超时?
PHP-FPM 场景下常误以为「连接池」是 Webman 自带的,其实 SMTP 客户端(如 phpmailer 或 symfony/mailer)默认每次 new 一个新连接,高频下发必然打满 SMTP 服务器连接数限制(比如腾讯企业邮限 20 并发)。
解法只有两个:
- 换用支持连接复用的客户端:推荐
symfony/mailer+smtp://user:pass@smtp.exmail.qq.com?timeout=10&keep_alive=true(keep_alive=true是关键) - 自己封装连接池:用
Swoole\Coroutine\Channel管理固定数量(如 5 个)的Swift_SmtpTransport实例,job 从 channel 获取 transport,用完$channel->push($transport)归还 - 千万别在
handle()开头new \PHPMailer(),一定要延迟到真正要发的时候再初始化,且确保$mail->smtpClose()被调用(协程环境下尤其重要)
连接池不是加个配置就能生效的东西,得看客户端底层是否真正复用 socket。很多号称“支持协程”的 SMTP 包只是把阻塞调用包进 go(),没解决连接爆炸问题。










