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-{对应选举年份},避免写入时因目录不存在抛出错误
方案一:队列分块处理(生产环境推荐)
接口层只负责任务派发,立即返回响应给前端,实际生成逻辑交给后台队列异步执行,完全没有超时限制:
- 接口内只做任务分发,不执行实际生成逻辑:
// 接口逻辑 $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' => '二维码生成任务已启动,可稍后查看结果']);
- 编写队列任务处理逻辑,每次只处理分片数据,分批插入释放内存:
// 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
相关产品推荐
相关产品推荐

