本文目录导读:

针对PHP项目的邮件队列与发送限额问题,通常是为了解决以下核心痛点:
- 高并发下API限流:SMTP服务商(如SendGrid、阿里云邮件)通常有每秒/每小时的发送上限。
- 慢速IO阻塞:发送邮件是网络I/O操作,同步发送会拖慢用户请求响应时间。
- 失败重试:临时性发送失败(如服务商返回429 Too Many Requests)需要自动重试。
以下是基于 PHP + Redis + Supervisor 的成熟解决方案架构与代码示例。
整体架构设计
[Web Request] -> [Queue Push] -> [Redis List] -> [Worker进程] -> [限流器] -> [SMTP Service]
-> [失败重入队列]
- 队列存储:Redis(PHP常用),也可用 RabbitMQ / Beanstalkd。
- Worker:PHP CLI 脚本,常驻运行(由 Supervisor 管理)。
- 限流机制:基于 Redis + Lua 实现令牌桶或滑动窗口。
核心代码实现
封装邮件投递类(DSendMail)
<?php
// MailSender.php
use Predis\Client as RedisClient;
class MailSender {
private RedisClient $redis;
private string $queueKey = 'email:queue';
// 限流参数
private int $maxPerSecond = 5; // 每秒最多5封
private int $maxPerHour = 1000; // 每小时最多1000封
public function __construct() {
$this->redis = new RedisClient([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
]);
}
/**
* 将邮件任务推入队列
*/
public function dispatch(string $to, string $subject, string $body, array $options = []): bool {
$task = [
'to' => $to,
'subject' => $subject,
'body' => $body,
'options' => $options,
'created_at' => time(),
'retry_count' => 0,
];
// 左推入队列(LPUSH)
$this->redis->lpush($this->queueKey, json_encode($task));
return true;
}
/**
* Worker 核心:消费队列,带限流
*/
public function consume(): void {
while (true) {
// 阻塞获取任务(BRPOP 无任务时阻塞)
$taskJson = $this->redis->brpop($this->queueKey, 5);
if (!$taskJson) continue;
$task = json_decode($taskJson[1], true);
// 等待限流许可
$this->waitForRateLimit();
// 执行发送
$result = $this->sendMail($task);
if ($result['success']) {
echo "Sent to {$task['to']}\n";
} else {
// 失败重试逻辑
$this->handleFailure($task, $result['error']);
}
}
}
/**
* 基于 Redis 的令牌桶限流器
*/
private function waitForRateLimit(): void {
$luaScript = <<<LUA
local key = KEYS[1]
local maxTokens = tonumber(ARGV[1]) -- 每秒令牌数
local bucketTime = tonumber(ARGV[2]) -- 令牌桶时间窗口(秒)
local current = redis.call('GET', key) or 0
if current < maxTokens then
redis.call('INCR', key)
redis.call('EXPIRE', key, bucketTime)
return 1 -- 允许发送
else
return 0 -- 限流
end
LUA;
// 秒级限流:每秒最多5封
while (true) {
$allowed = $this->redis->eval(
$luaScript,
1,
'email:rate:second',
5,
1
);
if ($allowed) break;
usleep(200000); // 等待200ms
}
// 小时级限流(类似原理)
// ...
}
private function sendMail(array $task): array {
try {
// 调用你的邮件发送逻辑(PHPMailer / Symfony Mailer / Guzzle API)
// 示例使用 Symfony Mailer
// $email = (new Email())->to($task['to'])->subject($task['subject'])->html($task['body']);
// $mailer->send($email);
return ['success' => true];
} catch (\Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
private function handleFailure(array $task, string $error): void {
$maxRetry = 3;
if ($task['retry_count'] < $maxRetry) {
$task['retry_count']++;
// 指数退避:延迟重试时间递增(秒)
$delay = pow(2, $task['retry_count']);
// 推入延迟队列(可利用 Redis 的有序集合实现延迟队列)
$this->redis->zadd('email:delay', time() + $delay, json_encode($task));
} else {
// 记录失败日志到数据库或日志文件
file_put_contents('/var/log/email_failure.log', json_encode($task) . PHP_EOL, FILE_APPEND);
}
}
}
Worker 启动脚本(CLI)
<?php // worker.php require 'vendor/autoload.php'; $sender = new MailSender(); echo "Email Worker started...\n"; $sender->consume();
Supervisor 配置
确保 Worker 进程崩溃后自动重启。
[program:email_worker] command=php /path/to/worker.php directory=/path/to/project autostart=true autorestart=true user=www-data numprocs=3 ; 启动3个进程同时消费 process_name=%(program_name)s_%(process_num)02d stdout_logfile=/var/log/email_worker.log stderr_logfile=/var/log/email_worker_error.log
进阶优化
使用更专业的限流库:nikic/php-rate-limiter
安装:composer require nikic/php-rate-limiter
use RateLimiter\Limiter;
use RateLimiter\Storage\RedisStorage;
$limiter = new Limiter(new RedisStorage($redisClient));
// 每秒5封,突发可10封
$limiter->allow('email:rate', 5, 10, 1);
基于 Laravel 的队列方案(开箱即用)
如果你的项目是 Laravel,可以直接用内置队列系统:
// 1. 配置 .env 使用 Redis 驱动
// QUEUE_CONNECTION=redis
// 2. 创建任务类
php artisan make:job SendEmailJob
// 3. Job 代码
public function handle()
{
// 限流中间件
RateLimiter::for('email', function () {
return Limit::perSecond(5)->perHour(1000);
});
Mail::to($this->user)->send($this->email);
}
// 4. 调度任务
SendEmailJob::dispatch($user, $email)->onQueue('emails');
异常重试策略
// 使用 Laravel 的重试机制
public $tries = 3; // 最多重试3次
public $backoff = [2, 10, 30]; // 重试间隔(秒)
// 或自定义重试逻辑
public function retryUntil(): DateTime
{
return now()->addMinutes(5); // 5分钟内重试
}
性能监控与告警
-
队列深度监控:
redis-cli LLEN email:queue
告警阈值:队列超过 10,000 封。 -
发送成功率:记录到 Prometheus / Grafana,
email_sent_total{status="success/fail"}。 -
限流触发次数:
email_rate_limit_hit_total。 -
Worker 状态:
supervisorctl status email_worker:*。
最佳实践建议
- 不要阻塞主请求:用户点击“发送验证码”后立即返回“发送中”,Worker 异步处理。
- 区分优先级:验证码类邮件高优先级(独立队列),营销邮件低优先级。
- 冷却时间:同一收件人发送间隔不少于 30 秒,避免被封。
- 日志完整:记录每封邮件的 Message ID、发送时间、耗时、错误原因。
- 邮箱验证:发送前验证邮箱格式(
filter_var($email, FILTER_VALIDATE_EMAIL))。
完整项目结构示例
/src
/MailSender.php # 队列推送 + 消费
/RateLimiter.php # 令牌桶实现
/FailureHandler.php # 失败重试逻辑
/worker.php # 入口脚本
/config/supervisor.conf # 守护进程配置
/scripts
/monitor.php # 队列深度监控
/retry_failed.php # 手动重试失败任务
通过以上方案,你可以轻松实现 高吞吐、低延迟、防封禁 的邮件发送系统,核心在于:异步队列解耦 + 限流保护 + 指数退避重试。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播