You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Laravel应用中为邮件服务实现轮询负载均衡及重试机制

问题需求

我目前有Postmark和SendGrid两个邮件发送服务,已经基于工厂模式实现了可扩展的灵活架构,包含EmailProviderInterface接口以及对应的实现类PostmarkEmailService、SendGridEmailService。当前的EmailServiceProvider是按优先级选择服务的,现在需要为其实现轮询(Round Robin)策略的负载均衡和重试机制,实现两个服务之间的自动切换;同时想了解如果引入队列系统,该如何应用这种负载均衡策略。


现有代码实现

EmailProviderInterface 接口

namespace App\Services\Notifications;

interface EmailProviderInterface
{
    public function sendEmail(string $to, string $templateName, array $data);
}

PostmarkEmailService 实现类

<?php

namespace App\Services\Notifications;

use App\Traits\SendEmailTrait;

class PostmarkEmailService implements EmailProviderInterface
{
    use SendEmailTrait;

    /**
     * 使用Postmark发送邮件
     *
     * @param string $to
     * @param string $templateName
     * @param array $data
     * @return array
     */
    public function sendEmail(string $to, string $templateName, array $data): array
    {

        $templateInfo = $this->getHtmlRender($templateName, $data);
        if (!$templateInfo['success']) {
            return $templateInfo;
        }
        
        try {
            // 调用外部API发送邮件的逻辑实现

            return $this->successHandler();
        } catch (\Exception $e) {
            return $this->handleErrors($e);
        }
    }
}

SendGridEmailService 实现类

<?php

namespace App\Services\Notifications;

use App\Traits\SendEmailTrait;

class SendGridEmailService implements EmailProviderInterface
{

    use SendEmailTrait;


    /**
     * 使用SendGrid发送邮件
     *
     * @param string $to
     * @param string $templateName
     * @param array $data
     * @return array
     */
    public function sendEmail(string $to, string $templateName, array $data): array
    {

        $templateInfo = $this->getHtmlRender($templateName, $data);
        if (!$templateInfo['success']) {
            return $templateInfo;
        }
        try {
            // 调用外部API发送邮件的逻辑实现

            return $this->successHandler();
        } catch (\Exception $e) {
            return $this->handleErrors($e);
        }
    }
}

当前的 EmailServiceProvider

<?php

namespace App\Services\Notifications;

class EmailServiceProvider
{
    private EmailProviderInterface $emailProvider;

    public function __construct(bool $highPriority = true)
    {
        if ($highPriority) {
            $this->emailProvider = new PostmarkEmailService();
        } else {
            $this->emailProvider = new SendGridEmailService();
        }
    }

    /**
     * 通过选定的邮件服务商发送邮件
     *
     * @param string $to
     * @param string $templateName
     * @param array $data
     * @return array
     */
    public function sendMail(string $to, string $templateName, array $data): array
    {
        return $this->emailProvider->sendEmail($to, $templateName, $data);
    }
}

解决方案实现

1. 轮询负载均衡+重试机制改造

改造EmailServiceProvider,引入轮询状态跟踪和重试逻辑,同时保持架构可扩展性:

<?php

namespace App\Services\Notifications;

class EmailServiceProvider
{
    private array $providers = [];
    private int $currentIndex = 0;
    private int $maxRetries;

    public function __construct(int $maxRetries = 2)
    {
        // 注册所有邮件服务商,新增服务商只需添加实例即可
        $this->providers = [
            new PostmarkEmailService(),
            new SendGridEmailService()
        ];
        $this->maxRetries = $maxRetries;

        // 可选:从分布式缓存(如Redis)读取上一次的索引,跨进程保持轮询状态
        // $this->currentIndex = cache()->get('email_provider_index', 0);
    }

    /**
     * 轮询获取下一个邮件服务商
     * @return EmailProviderInterface
     */
    private function getNextProvider(): EmailProviderInterface
    {
        $provider = $this->providers[$this->currentIndex];
        // 更新索引,循环轮询
        $this->currentIndex = ($this->currentIndex + 1) % count($this->providers);
        
        // 可选:持久化当前索引,避免进程重启后轮询状态重置
        // cache()->put('email_provider_index', $this->currentIndex);
        
        return $provider;
    }

    /**
     * 带重试和轮询的邮件发送逻辑
     * @param string $to
     * @param string $templateName
     * @param array $data
     * @return array
     */
    public function sendMail(string $to, string $templateName, array $data): array
    {
        $attempts = 0;
        $lastError = [];

        while ($attempts < $this->maxRetries) {
            $provider = $this->getNextProvider();
            $result = $provider->sendEmail($to, $templateName, $data);

            if ($result['success']) {
                return $result;
            }

            $lastError = $result;
            $attempts++;
        }

        // 所有重试失败,返回最后一次的错误信息
        return $lastError;
    }
}

核心说明:

  • 轮询逻辑:通过currentIndex跟踪当前服务商,每次调用后索引自增取模,实现循环切换
  • 重试机制:达到最大重试次数前,自动切换服务商重试
  • 可扩展性:新增服务商只需在providers数组中添加实例,无需修改核心逻辑
  • 跨进程状态:如果需要多进程(如CLI、队列)保持统一轮询顺序,必须用分布式缓存存储currentIndex

2. 引入队列后的负载均衡应用

结合队列系统(如Laravel Queue)时,轮询策略有两种应用方式:

方式1:任务创建时指定轮询服务商

在触发队列任务前,先获取轮询后的服务商,将类名传入任务,任务执行时直接使用该服务商:

// 邮件发送任务类
class SendEmailJob implements ShouldQueue
{
    private string $to;
    private string $templateName;
    private array $data;
    private string $providerClass;

    public function __construct(string $to, string $templateName, array $data, string $providerClass)
    {
        $this->to = $to;
        $this->templateName = $templateName;
        $this->data = $data;
        $this->providerClass = $providerClass;
    }

    public function handle()
    {
        /** @var EmailProviderInterface $provider */
        $provider = app($this->providerClass);
        $provider->sendEmail($this->to, $this->templateName, $this->data);
    }
}

// 触发任务时获取轮询后的服务商
$emailService = app(EmailServiceProvider::class);
$nextProvider = $emailService->getNextProvider();
dispatch(new SendEmailJob($to, $templateName, $data, get_class($nextProvider)));

方式2:任务执行时内部处理轮询

任务中直接调用改造后的EmailServiceProvider,由服务内部处理轮询和重试:

class SendEmailJob implements ShouldQueue
{
    private string $to;
    private string $templateName;
    private array $data;

    public function __construct(string $to, string $templateName, array $data)
    {
        $this->to = $to;
        $this->templateName = $templateName;
        $this->data = $data;
    }

    public function handle(EmailServiceProvider $emailService)
    {
        $result = $emailService->sendMail($this->to, $this->templateName, $this->data);
        if (!$result['success']) {
            // 根据错误类型决定是否重新入队
            $this->fail(new \Exception('邮件发送失败: ' . $result['message']));
        }
    }
}

队列场景注意事项:

  • 多队列进程下,必须用分布式缓存存储currentIndex,避免各进程维护独立索引导致轮询混乱
  • 可结合队列自身的重试机制:服务内部重试1次,队列再重试1次,分层处理失败场景

内容的提问来源于stack exchange,提问作者K.A.S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 02:17:07