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

Laravel批量生成唯一二维码报503错误的最佳实现方案咨询

Laravel批量生成成员二维码超时、重复问题解决方案

问题根因

你修改php.ini的max_execution_time后仍返回503,核心原因有三个:

  • 503是Web服务层拦截返回的,不是PHP层面超时:Nginx的proxy_read_timeout、Apache的Timeout、PHP-FPM的request_terminate_timeout默认超时阈值大多在30-60秒,没有同步修改这部分配置的话,请求到时间会被服务直接掐断,和PHP配置无关
  • 代码一次性加载4800+条全量成员记录到内存,循环过程中持续生成二维码二进制内容、攒批量插入数组,内存占用持续升高,容易触发内存限制被中断
  • 现有逻辑存在重复风险:用time()作为文件名生成依据,循环执行速度快时同一秒内会生成完全相同的文件名,直接覆盖之前生成的二维码;Str::random()没有做唯一校验,极端场景会出现hash碰撞重复

可落地方案

核心原则是不要把长耗时批量任务放在同步HTTP请求里执行,从架构层面规避超时,同时加多层校验避免重复:

前置准备

  • 先给ballots表的qr_hash字段加唯一索引,从数据库层面彻底阻断重复hash写入:
// 新增迁移执行索引添加
Schema::table('ballots', function (Blueprint $table) {
    $table->string('qr_hash', 33)->unique();
});
  • 提前创建好二维码存储目录storage/app/qr-code/election-{对应选举年份},避免写入时因目录不存在抛出错误

方案一:队列分块处理(生产环境推荐)

接口层只负责任务派发,立即返回响应给前端,实际生成逻辑交给后台队列异步执行,完全没有超时限制:

  1. 接口内只做任务分发,不执行实际生成逻辑:
// 接口逻辑
$total = \DB::connection('mysql2')->table('basicinfo')
    ->where('is_approved', '1')
    ->whereIn('classOfMembership', $member_arr)
    ->count();
// 按每100条一个任务拆分,避免单任务负载过高
for ($offset = 0; $offset < ceil($total / 100); $offset++) {
    GenerateQrBatch::dispatch(
        $request->election_year,
        $request->post,
        $member_arr,
        $offset * 100,
        100
    );
}
return response()->json(['message' => '二维码生成任务已启动,可稍后查看结果']);
  1. 编写队列任务处理逻辑,每次只处理分片数据,分批插入释放内存:
// app/Jobs/GenerateQrBatch.php 处理逻辑
public function handle()
{
    // 每次只查询当前分片的100条数据,不加载全量
    $members = \DB::connection('mysql2')->table('basicinfo')
        ->where('is_approved', '1')
        ->whereIn('classOfMembership', $this->memberArr)
        ->offset($this->offset)
        ->limit($this->limit)
        ->get();

    $insertRows = [];
    foreach ($members as $member) {
        // 循环校验生成不重复的随机hash
        do {
            $randomStr = Str::random(33);
            $exists = Ballot::where('qr_hash', $randomStr)->exists();
        } while ($exists);

        // 用唯一hash拼接文件名,彻底避免重名覆盖
        $imageName = sprintf('img-%s.png', $randomStr);
        $filePath = sprintf('/qr-code/election-%s/%s', $this->electionYear, $imageName);

        // 生成二维码写入存储
        $qrImage = QrCode::format('png')
            ->size(200)
            ->errorCorrection('H')
            ->generate('https://example.com/' . $randomStr);
        Storage::disk('local')->put($filePath, $qrImage);

        $insertRows[] = [
            'election_year' => $this->electionYear,
            'position' => $this->post,
            'qr_hash' => $randomStr,
            'qr_code_image' => $imageName
        ];

        // 每凑够20条就插入一次,清空数组释放内存,不要攒全量再插入
        if (count($insertRows) >= 20) {
            Ballot::insert($insertRows);
            $insertRows = [];
        }
    }
    // 插入剩余不足20条的记录
    if (!empty($insertRows)) {
        Ballot::insert($insertRows);
    }
}

方案二:CLI命令行执行(快速落地,无需配队列)

如果是内部使用场景,不想配置队列,可以把生成逻辑写到Artisan自定义命令中,通过命令行执行:

  • CLI模式下PHP默认不限制执行时间,也没有Nginx、PHP-FPM的超时拦截,不会出现503
  • 查询数据时用chunkById分块拉取,不要一次性加载全量:
// 命令handle方法内逻辑
\DB::connection('mysql2')->table('basicinfo')
    ->where('is_approved', '1')
    ->whereIn('classOfMembership', $member_arr)
    ->chunkById(100, function($members) use ($electionYear, $post) {
        // 单块100条数据的生成、插入逻辑和队列任务内逻辑一致
    });

编写完成后在服务器命令行执行php artisan qrcode:generate即可跑完全部生成逻辑。

避坑提醒

  • 绝对不要用时间戳作为文件名的唯一标识,循环执行速度快时重名概率极高,直接用唯一的qr_hash拼接文件名是最稳妥的方案
  • 数据库层面的唯一索引必须加,代码层的校验无法覆盖极端并发场景,唯一索引是防重复的最后一道防线
  • 不要攒全量插入数据再一次性写库,分批次插入可以大幅降低内存占用,避免内存溢出

内容的提问来源于stack exchange,提问作者user3418427

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 17:36:20