PHP项目邮件队列与发送限额

wen PHP项目 4

本文目录导读:

PHP项目邮件队列与发送限额

  1. 整体架构设计
  2. 核心代码实现
  3. 进阶优化
  4. 性能监控与告警
  5. 最佳实践建议
  6. 完整项目结构示例

针对PHP项目的邮件队列发送限额问题,通常是为了解决以下核心痛点:

  1. 高并发下API限流:SMTP服务商(如SendGrid、阿里云邮件)通常有每秒/每小时的发送上限。
  2. 慢速IO阻塞:发送邮件是网络I/O操作,同步发送会拖慢用户请求响应时间。
  3. 失败重试:临时性发送失败(如服务商返回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分钟内重试
}

性能监控与告警

  1. 队列深度监控redis-cli LLEN email:queue
    告警阈值:队列超过 10,000 封。

  2. 发送成功率:记录到 Prometheus / Grafana,email_sent_total{status="success/fail"}

  3. 限流触发次数email_rate_limit_hit_total

  4. Worker 状态supervisorctl status email_worker:*


最佳实践建议

  1. 不要阻塞主请求:用户点击“发送验证码”后立即返回“发送中”,Worker 异步处理。
  2. 区分优先级:验证码类邮件高优先级(独立队列),营销邮件低优先级。
  3. 冷却时间:同一收件人发送间隔不少于 30 秒,避免被封。
  4. 日志完整:记录每封邮件的 Message ID、发送时间、耗时、错误原因。
  5. 邮箱验证:发送前验证邮箱格式(filter_var($email, FILTER_VALIDATE_EMAIL))。

完整项目结构示例

/src
  /MailSender.php       # 队列推送 + 消费
  /RateLimiter.php      # 令牌桶实现
  /FailureHandler.php   # 失败重试逻辑
/worker.php             # 入口脚本
/config/supervisor.conf # 守护进程配置
/scripts
  /monitor.php          # 队列深度监控
  /retry_failed.php     # 手动重试失败任务

通过以上方案,你可以轻松实现 高吞吐、低延迟、防封禁 的邮件发送系统,核心在于:异步队列解耦 + 限流保护 + 指数退避重试

抱歉,评论功能暂时关闭!