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

如何避免批量发送带工资单附件邮件时SendGrid被限流?

解决方案建议

针对你遇到的SendGrid限流和Azure Function超时问题,结合带唯一附件的场景,给出以下实用方案:

1. 改用SendGrid批量发送接口

放弃循环中单封发送的方式,改用SendGrid批量邮件接口,将多封邮件打包成一个API请求发送,大幅减少调用次数,从根源上降低触发限流的概率。

实现示例:

var sendGridMsg = new SendGridMessage();
sendGridMsg.SetFrom(new EmailAddress("your-sender@domain.com", "公司名称"));
sendGridMsg.SetTemplateId("你的邮件模板ID");

foreach (var employee in employeeList)
{
    // 为每个员工创建独立的个性化配置
    var personalization = new Personalization();
    personalization.AddTo(new EmailAddress(employee.Email, employee.Name));
    // 注入模板所需的个性化数据(比如员工姓名、工号等)
    personalization.SetDynamicTemplateData("employeeName", employee.Name);
    
    // 添加该员工的唯一工资单附件
    var payslipAttachment = new Attachment
    {
        Content = Convert.ToBase64String(employee.PayslipFileContent),
        Filename = $"工资单_{employee.Id}_{DateTime.Now:yyyyMMdd}.pdf",
        Type = "application/pdf",
        Disposition = "attachment"
    };
    personalization.AddAttachment(payslipAttachment);
    
    sendGridMsg.AddPersonalization(personalization);
}

// 一次请求发送所有邮件
await sendGridClient.SendEmailAsync(sendGridMsg);

2. 实现智能限流重试机制

即使使用批量接口,仍可能遇到SendGrid的限流(429状态码),需添加重试逻辑并遵守SendGrid的限流头部信息:

  • 用Polly库实现指数退避重试,避免频繁重试加剧限流
  • 解析响应头中的X-RateLimit-Reset值,精准等待到限流重置时间后再重试

示例代码:

var retryPolicy = Policy
    .Handle<SendGridException>(ex => ex.StatusCode == HttpStatusCode.TooManyRequests)
    .WaitAndRetryAsync(3, (retryCount, context) =>
    {
        // 尝试从异常中获取限流重置时间,没有则用指数退避
        if (context.Exception is SendGridException sgEx && 
            sgEx.ResponseHeaders.TryGetValue("X-RateLimit-Reset", out var resetTimeStr) &&
            long.TryParse(resetTimeStr, out var resetEpoch))
        {
            var resetTime = DateTimeOffset.FromUnixTimeSeconds(resetEpoch).UtcDateTime;
            return resetTime - DateTime.UtcNow;
        }
        return TimeSpan.FromSeconds(Math.Pow(2, retryCount));
    });

await retryPolicy.ExecuteAsync(async () =>
{
    await sendGridClient.SendEmailAsync(sendGridMsg);
});

3. 用Azure Durable Functions解决超时问题

普通Azure Function有严格的超时限制(消费计划默认5分钟,最大10分钟),改用Durable Functions的Fan-Out/Fan-In模式拆分任务:

  • 创建Orchestrator函数,将90名员工分成多个批次(比如每15个一批)
  • 每个批次调用独立的Activity函数发送邮件,Orchestrator负责协调所有批次的执行
  • Durable Functions支持长时间运行的工作流,不会因为单个Activity超时导致整个任务终止

核心逻辑示例:

// Orchestrator函数
[FunctionName("PayslipEmailOrchestrator")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var employeeList = context.GetInput<List<Employee>>();
    // 拆分成每15个员工一批
    var batches = employeeList.Chunk(15);
    
    var tasks = new List<Task>();
    foreach (var batch in batches)
    {
        tasks.Add(context.CallActivityAsync("SendPayslipBatch", batch));
    }
    
    // 等待所有批次完成
    await Task.WhenAll(tasks);
}

// Activity函数
[FunctionName("SendPayslipBatch")]
public static async Task SendPayslipBatch(
    [ActivityTrigger] List<Employee> batch,
    ILogger log)
{
    // 这里复用前面的批量发送逻辑发送当前批次的邮件
    var sendGridClient = new SendGridClient(Environment.GetEnvironmentVariable("SendGridApiKey"));
    // ... 构造批量邮件并发送
}

4. 优化附件预处理逻辑

避免在发送循环中实时生成或读取附件,提前完成工资单的生成和存储:

  • 提前将所有员工的工资单PDF生成并上传到Azure Blob Storage
  • 为每个Blob生成临时SAS链接,在邮件中直接引用链接(或在批量请求中嵌入Base64编码的内容)
  • 这样能减少发送逻辑中的IO操作,提升整体处理效率

5. 临时调整Azure Function配置(可选)

如果暂时无法切换到Durable Functions,可以尝试调整普通Function的超时时间:

  • 在host.json中设置functionTimeout为消费计划允许的最大值(00:10:00)
  • 注意这只是临时方案,长期来看Durable Functions更可靠

内容的提问来源于stack exchange,提问作者Tho-venaar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 11:50:22