1. 项目背景与核心需求在Web开发中邮件发送是常见的业务需求但当需要批量发送大量邮件时传统的同步处理方式会面临几个关键问题执行时间过长假设每封邮件发送耗时2秒发送1000封邮件就需要约33分钟这会导致HTTP请求超时资源占用高长时间运行的PHP进程会占用服务器内存和CPU资源可靠性问题如果发送过程中脚本意外终止难以恢复中断的任务本项目通过PHP CLI模式结合MySQL实现了一个轻量级的异步邮件队列系统核心解决以下问题将耗时的邮件发送任务转移到后台异步执行通过数据库记录任务状态确保不丢失发送记录支持任务重试机制提高发送成功率在Linux环境下实现稳定的后台进程管理2. 技术架构设计2.1 整体架构系统采用生产者-消费者模型[Web请求] → [MySQL任务表] ← [CLI消费者进程]生产者Web应用将待发送邮件信息写入MySQL的task_list表消费者CLI脚本从表中读取待发送任务执行发送并更新状态2.2 数据库设计核心表结构如下CREATE TABLE task_list ( id int(11) NOT NULL AUTO_INCREMENT, user_email varchar(255) NOT NULL, subject varchar(255) NOT NULL, content text NOT NULL, status tinyint(1) DEFAULT 0 COMMENT 0-待发送 1-发送成功 2-发送失败, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8;2.3 关键技术选型PHP CLI模式相比Web模式CLI具有无超时限制可后台持续运行更低的资源开销进程管理nohup保证SSH断开后进程继续运行符号实现后台执行输出重定向到/dev/null避免日志文件膨胀邮件发送使用PHPMailer库支持SMTP协议内置HTML内容和附件支持3. 核心实现步骤3.1 生产者端实现Web应用中添加任务的典型代码// 添加邮件发送任务 public function addEmailTask($email, $subject, $content) { $data [ user_email $email, subject $subject, content $content, status 0 ]; return M(TaskList)-add($data); }3.2 消费者端实现CLI脚本核心逻辑// cli.php ?php require_once __DIR__./vendor/autoload.php; $model M(TaskList); while (true) { // 获取待发送任务 $tasks $model-where([status 0])-limit(10)-select(); if (empty($tasks)) { sleep(10); // 无任务时休眠 continue; } foreach ($tasks as $task) { try { $result send_email($task[user_email], $task[subject], $task[content]); // 更新状态 $updateData [status $result[error] ? 2 : 1]; $model-where([id $task[id]])-save($updateData); sleep(1); // 控制发送频率 } catch (Exception $e) { // 记录错误日志 file_put_contents(mail_error.log, date(Y-m-d H:i:s). .$e-getMessage().PHP_EOL, FILE_APPEND); } } }3.3 邮件发送函数增强版的邮件发送实现function send_email($address, $subject, $content) { $mail new PHPMailer(true); try { // 服务器配置 $mail-isSMTP(); $mail-Host smtp.example.com; $mail-SMTPAuth true; $mail-Username userexample.com; $mail-Password password; $mail-SMTPSecure PHPMailer::ENCRYPTION_SMTPS; $mail-Port 465; // 收件人 if (is_array($address)) { foreach ($address as $email) { $mail-addAddress(trim($email)); } } else { $mail-addAddress(trim($address)); } // 内容 $mail-isHTML(true); $mail-Subject $subject; $mail-Body $content; $mail-send(); return [error 0]; } catch (Exception $e) { return [ error 1, message Mailer Error: {$mail-ErrorInfo} ]; } }4. Linux环境部署实践4.1 启动消费者进程使用nohup保持进程长期运行nohup php /path/to/cli.php /dev/null 21 关键参数说明 /dev/null丢弃标准输出21将错误输出重定向到标准输出后台运行4.2 进程监控与管理查看运行中的进程ps aux | grep cli.php停止进程kill -9 [PID]使用Supervisor管理推荐[program:mail_queue] commandphp /path/to/cli.php directory/path/to/ autostarttrue autorestarttrue userwww-data redirect_stderrtrue stdout_logfile/var/log/mail_queue.log4.3 性能优化建议批量处理每次从数据库读取多条记录如10-20条减少查询次数连接复用保持SMTP连接而不是每次新建错峰发送在高并发时段增加sleep时间失败重试对失败任务设置最大重试次数5. 实战问题与解决方案5.1 常见问题排查权限问题确保PHP有执行权限chmod x cli.php检查目录写入权限环境问题CLI和Web可能使用不同的php.ini使用php -i | grep php.ini确认CLI配置文件路径邮件发送失败检查SMTP服务器日志测试Telnet连接telnet smtp.example.com 4655.2 高级功能扩展优先级队列ALTER TABLE task_list ADD priority TINYINT DEFAULT 0;查询时按优先级排序$tasks $model-where([status 0])-order(priority DESC)-select();发送速率限制// 控制在每分钟不超过30封 $lastMinuteCount $model-where([ status 1, created_at [gt, date(Y-m-d H:i:s, strtotime(-1 minute))] ])-count(); if ($lastMinuteCount 30) { sleep(60); }邮件模板支持function render_template($template, $data) { extract($data); ob_start(); include templates/$template.php; return ob_get_clean(); }6. 性能测试数据测试环境服务器2核4G Ubuntu 20.04MySQL5.7PHP7.4测试结果邮件数量同步方式耗时队列方式耗时1003m28s0.5s(Web) 2m10s(后台)50017m15s0.5s(Web) 10m45s(后台)1000超时(30m)0.6s(Web) 22m30s(后台)关键优势Web响应时间从分钟级降到秒级系统资源占用更平稳支持任务中断恢复7. 安全注意事项SQL注入防护使用预处理语句$model-where(status :status)-bind(:status, 0)-select();邮件内容安全HTML内容过滤$content htmlspecialchars($content, ENT_QUOTES);附件类型检查$allowedTypes [pdf, doc, jpg]; if (!in_array($fileType, $allowedTypes)) { throw new Exception(Invalid file type); }进程隔离使用单独的系统用户运行限制文件系统访问权限8. 替代方案对比方案优点缺点适用场景本方案简单、无需额外组件需自行管理进程小到中型应用Redis队列高性能、支持分布式需要Redis服务器高并发场景RabbitMQ专业可靠、功能丰富系统复杂度高企业级应用第三方邮件服务API无需维护发送逻辑依赖第三方、成本高快速实现、预算充足9. 实际应用建议日志记录记录发送成功/失败状态记录执行时间用于性能分析监控告警监控进程是否存活监控队列积压情况数据清理-- 定期清理已完成的任务 DELETE FROM task_list WHERE status 1 AND created_at DATE_SUB(NOW(), INTERVAL 30 DAY);多语言支持$mail-CharSet UTF-8; $mail-Encoding base64;10. 完整部署示例初始化数据库CREATE TABLE task_list ( id int(11) NOT NULL AUTO_INCREMENT, user_email varchar(255) NOT NULL, subject varchar(255) NOT NULL, content text NOT NULL, status tinyint(1) DEFAULT 0, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;安装依赖composer require phpmailer/phpmailer启动脚本使用Supervisor[program:mail_queue] commandphp /var/www/mail_queue/cli.php directory/var/www/mail_queue autostarttrue autorestarttrue userwww-data numprocs2 process_name%(program_name)s_%(process_num)02d redirect_stderrtrue stdout_logfile/var/log/mail_queue.log测试发送// 添加测试任务 $data [ user_email testexample.com, subject 测试邮件, content h1这是一封测试邮件/h1 ]; M(TaskList)-add($data); // 查看状态 $task M(TaskList)-find(); echo $task[status]; // 0-待处理 1-成功 2-失败11. 关键经验总结数据库连接管理CLI长期运行需注意MySQL连接超时问题建议添加自动重连逻辑function checkDbConnection() { try { M()-query(SELECT 1); } catch (Exception $e) { M()-close(); M()-connect(); } }内存泄漏预防长时间运行的脚本需定期释放资源每处理100次任务后重启脚本if ($count % 100 0) { exit(0); // 由Supervisor重新启动 }信号处理pcntl_signal(SIGTERM, function() { // 清理工作 exit(0); }); while (true) { pcntl_signal_dispatch(); // 业务逻辑 }性能瓶颈识别使用XHProf分析php -d xhprof.output_dir/tmp -d xhprof.sampling_interval10000 cli.php邮件服务商选择国内推荐阿里云邮件推送、SendCloud国际推荐SendGrid、Mailgun自建要求有固定IP、配置SPF/DKIM/DMARC这套方案在实际项目中已经稳定运行超过3年日均处理邮件量在5万封左右峰值时可达到每小时1万封。核心优势在于简单可靠不需要引入额外的消息队列组件适合大多数PHP项目的快速集成。对于更高要求的场景可以考虑替换为专业的队列系统如RabbitMQ但基本原理和架构思想仍然适用。