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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;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 = 'user@example.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 2>&1 &关键参数说明:
> /dev/null:丢弃标准输出2>&1:将错误输出重定向到标准输出&:后台运行
4.2 进程监控与管理
- 查看运行中的进程:
ps aux | grep 'cli.php'- 停止进程:
kill -9 [PID]- 使用Supervisor管理(推荐):
[program:mail_queue] command=php /path/to/cli.php directory=/path/to/ autostart=true autorestart=true user=www-data redirect_stderr=true stdout_logfile=/var/log/mail_queue.log4.3 性能优化建议
- 批量处理:每次从数据库读取多条记录(如10-20条),减少查询次数
- 连接复用:保持SMTP连接而不是每次新建
- 错峰发送:在高并发时段增加sleep时间
- 失败重试:对失败任务设置最大重试次数
5. 实战问题与解决方案
5.1 常见问题排查
权限问题:
- 确保PHP有执行权限:
chmod +x cli.php - 检查目录写入权限
- 确保PHP有执行权限:
环境问题:
- CLI和Web可能使用不同的php.ini
- 使用
php -i | grep 'php.ini'确认CLI配置文件路径
邮件发送失败:
- 检查SMTP服务器日志
- 测试Telnet连接:
telnet smtp.example.com 465
5.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.04
- MySQL:5.7
- PHP:7.4
测试结果:
| 邮件数量 | 同步方式耗时 | 队列方式耗时 |
|---|---|---|
| 100 | 3m28s | 0.5s(Web) + 2m10s(后台) |
| 500 | 17m15s | 0.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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;- 安装依赖:
composer require phpmailer/phpmailer- 启动脚本(使用Supervisor):
[program:mail_queue] command=php /var/www/mail_queue/cli.php directory=/var/www/mail_queue autostart=true autorestart=true user=www-data numprocs=2 process_name=%(program_name)s_%(process_num)02d redirect_stderr=true stdout_logfile=/var/log/mail_queue.log- 测试发送:
// 添加测试任务 $data = [ 'user_email' => 'test@example.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_interval=10000 cli.php邮件服务商选择:
- 国内推荐:阿里云邮件推送、SendCloud
- 国际推荐:SendGrid、Mailgun
- 自建要求:有固定IP、配置SPF/DKIM/DMARC
这套方案在实际项目中已经稳定运行超过3年,日均处理邮件量在5万封左右,峰值时可达到每小时1万封。核心优势在于简单可靠,不需要引入额外的消息队列组件,适合大多数PHP项目的快速集成。对于更高要求的场景,可以考虑替换为专业的队列系统如RabbitMQ,但基本原理和架构思想仍然适用。