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

Laravel 10万级邮件群发结果行分块处理的最佳实践是什么

性能问题根因分析
  • 一次性加载10万条BlastLog模型到内存,内存占用过高,频繁GC拖累执行效率
  • 遍历每条记录时单独执行BlastLog的save()操作,产生10万次单条更新SQL,数据库IO成为严重瓶颈
  • 构造BlastEmail时重复查询关联的Blast模型,产生多余的10万次SQL查询
  • 未充分利用批量处理和队列并发能力,执行效率上限低
优化方案

1. 用chunk分块处理关联数据

不要直接调用$blast->unsentLogs关联属性(会一次性加载所有数据),而是调用关联方法$blast->unsentLogs()返回查询构造器,使用Laravel内置的chunk方法分块加载,建议单块大小设置为100~500,可根据服务器内存灵活调整。
优化后的handle方法代码:

public function handle()
{
    $blastId = $this->argument('blast');

    $blast = Blast::findOrFail($blastId);
    activity()->performedOn($blast)->log('Blast is sending');
    
    // 分块加载未发送记录,单块200条,同时预加载contact关联避免N+1查询
    $blast->unsentLogs()
        ->with('contact')
        ->chunk(200, function ($logs) use ($blast) {
            foreach ($logs as $log) {
                try {
                    // 直接传入公共的$blast对象,避免每条记录重复查询Blast
                    Mail::queue((new BlastEmail($log, $blast))->onQueue('emails'));
                } catch (Exception $e) {
                    activity('send_failed')->performedOn($log->contact)->causedBy($log)->log('Email send failed: '.$e->getMessage());
                }
            }
        });

    // 注意:此处直接标记为sent仅代表已全部推送到队列,而非全部发送完成
    // 如需标记真实发送完成,建议配合队列完成事件统计实现
    $blast->status = 'sent';
    $blast->save();
    activity()->performedOn($blast)->log('Blast is pushed to queue');

    return 0;
}

2. 优化BlastEmail构造逻辑

  • 不要在构造函数中更新sent_at字段:Mail::queue仅将邮件序列化后存入队列,此时邮件并未实际发送,提前标记为已发送会导致发送失败的记录也被误标为已发送
  • 移除构造函数中查询Blast模型的逻辑,直接传入外部已查询到的$blast对象,减少重复SQL
  • sent_at更新逻辑建议迁移到邮件发送成功的回调中,可通过Laravel的MessageSent事件监听实现,也可以在队列任务执行完成后更新

优化后的构造函数代码:

public function __construct(BlastLog $log, Blast $blast)
{
    $contact = $log->contact;
    // 直接使用传入的blast对象,无需重复查询
    $this->content = str_replace('**UNSUB**', 'https://urltounsubscribe.com', $blast->content);
    $this->subject = $blast->subject;
    $this->to($contact->email, $contact->name ?? 'Name');
    $this->from($blast->from_email, $blast->from_name);
}

3. 队列配置优化

  • 队列驱动优先使用Redis,相比Database驱动有更高的吞吐性能
  • 启动多个队列工作进程消费emails队列,例如启动20个进程:php artisan queue:work --queue=emails --tries=3,可根据服务器配置和邮件服务商限流规则调整进程数量
  • 合理设置队列任务超时、重试次数,避免失败任务阻塞队列

4. 额外优化建议

  • 如果邮件服务商支持批量发送接口,优先使用批量接口推送,减少API调用次数
  • 可将邮件公共内容的处理逻辑提前执行,不需要每条都重复替换相同的基础内容,仅替换个性化部分即可
  • 数据库层面为BlastLog的blast_id、发送状态字段添加索引,提升分块查询的效率

内容的提问来源于stack exchange,提问作者Muhammad Dyas Yaskur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 05:45:04