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

Laravel Hostinger共享主机队列报MaxAttemptsExceededException异常

Hostinger共享主机部署Laravel队列任务执行失败/卡死问题

问题现象

  • 部署环境:Hostinger共享主机,本地localhost环境Excel导入队列功能运行完全正常,仅服务器端出现异常
  • 异常表现:终端执行php artisan queue:work命令后进程被强制终止,队列任务要么直接失败,要么长期处于运行中状态,多次排查未解决
  • 报错信息:
Illuminate\Queue\MaxAttemptsExceededException: App\Jobs\ImportJob has been attempted too many times or run too long. The job may have previously timed out.
  • 业务调用链路:Livewire组件Import调度ImportJob队列任务,最终调用ProductImport完成Maatwebsite Excel批量数据导入

核心业务代码

Import.php(Livewire调度类)

<?php

namespace App\Http\Livewire;

use App\Jobs\ImportJob;
use Modules\Product\Entities\Product;
use Modules\Product\Entities\Years;
use Modules\Product\Entities\Glasses;
use Modules\Product\Entities\Parts;
use Modules\Product\Entities\Bodystyles;
use Modules\Product\Entities\Models;
use Modules\Product\Entities\Makers;
use Modules\Product\Entities\Features;
use Modules\Product\Entities\BodystylesModels;
use Livewire\Component;
use Livewire\WithFileUploads;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Storage;

class Import extends Component
{
    use WithFileUploads;

    public $batchId;
    public $importFile;
    public $importing = false;
    public $importFilePath;
    public $importFinished = false;

    public function import()
    {
        $this->validate([
            'importFile' => 'required',
        ]);

        // Product::query()->truncate();
        // Years::query()->truncate();
        // Makers::query()->truncate();
        // Models::query()->truncate();
        // Bodystyles::query()->truncate();
        // Glasses::query()->truncate();
        // Parts::query()->truncate();
        // Features::query()->truncate();
        

        $this->importing = true;
        $this->importFilePath = $this->importFile->store('imports');

        $batch = Bus::batch([
            new ImportJob($this->importFilePath),
        ])->dispatch();

        $this->batchId = $batch->id;
    }

    public function getImportBatchProperty()
    {
        if (!$this->batchId) {
            return null;
        }

        return Bus::findBatch($this->batchId);
    }

    public function updateImportProgress()
    {
        $this->importFinished = $this->importBatch->finished();

        if ($this->importFinished) {
            Storage::delete($this->importFilePath);
            $this->importing = false;
        }
    }

    public function render()
    {
        return view('livewire.import');
    }
}

ImportJob.php(队列任务类)

<?php

namespace App\Jobs;

use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use App\Imports\ProductImport;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class ImportJob implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $uploadFile;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($uploadFile)
    {
        $this->uploadFile = $uploadFile;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        Excel::import(new ProductImport, $this->uploadFile);
    }

    /**
     * The job failed to process.
     *
     * @param  Exception  $exception
     * @return void
     */
    public function failed(Exception $exception)
    {
        dd($exception);
    }
}

ProductImport.php(Excel导入实现类)

<?php

namespace App\Imports;

use Modules\Product\Entities\Product;
use Modules\Product\Entities\Years;
use Modules\Product\Entities\Glasses;
use Modules\Product\Entities\Parts;
use Modules\Product\Entities\Bodystyles;
use Modules\Product\Entities\Models;
use Modules\Product\Entities\Makers;
use Modules\Product\Entities\Features;
use Modules\Product\Entities\BodystylesModels;
use Maatwebsite\Excel\Concerns\ToModel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithBatchInserts;
use Maatwebsite\Excel\Concerns\WithChunkReading;

class ProductImport implements ToModel, WithHeadingRow, WithChunkReading
{
    private $users;

    public function __construct()
    {
        
        // $this->users = User::all(['id', 'name'])->pluck('id', 'name');
    }

    public function model(array $row)
    {
        
        // MakersFeatures::
        
        $year= Years::where('years_name', trim($row['year'],''))->first();
        
        $make= Makers::where('makers_name', trim($row['make'],''))->first();
        $model= Models::where('models_name', trim($row['model'],''))->first();
        $style= Bodystyles::where('bodystyles_name', trim($row['style'],''))->first();
        $glass= Glasses::where('glasses_name', trim($row['glass'],''))->first();
        $part= Parts::where('parts_name', trim($row['partnumber'],''))->first();
        $feature= Features::where('features_name', trim($row['feature'],''))->first();

        if($year==null)
        {
            $year= Years::create(['years_name'=> trim($row['year'],'')]);
            
        }
        if($make==null)
        {
            $make= Makers::create(['makers_name'=> trim($row['make'],'')]);
        }
    
        if($model==null)
        {
            $model= Models::create(
                [
                    'models_name'=> trim($row['model'],''),
                    'makers_id'=>$make->id
    
                ]);
        }

        if($style==null)
        {
            $style= Bodystyles::create(['bodystyles_name'=> trim($row['style'],'')]);
        }

        if($glass==null)
        {
               
            $glass= Glasses::create(['glasses_name'=>trim($row['glass'],'')]);
        }

        if($part==null)
        {
               
            $part= Parts::create(['parts_name'=>trim($row['partnumber'],'')]);

        }
        if($feature==null)
        {
            $feature= Features::create([
                'features_name'=>trim($row['feature'],''),
                'glasses_id'=>$glass->id
            ]);
        }

        

        $f=
        [
            'years_id'=>$year->id,
            'makers_id'=>$make->id, 
            'models_id'=>$model->id, 
            'bodystyles_id'=>$style->id, 
            'glasses_id'=>$glass->id, 
            'part_id'=>$part->id,
            'product_part_number'=>trim($row['partnumber'],''),
            'product_price'=>trim($row['sale_price'],''),
            'features_id'=> $feature->id
        ];
        
        // $d= MakersFeatures::create($f);
  
        return new Product($f);
    }

    public function chunkSize(): int
    {
        return 2000;
    }
}

问题根因及解决方案

该问题是共享主机环境的典型限制导致,和代码逻辑本身无关,按以下优先级调整即可解决:

  • 根本原因:Hostinger共享主机会强制杀掉运行时长超过60秒(部分基础套餐为30秒)的CLI进程,同时对单进程内存占用有严格上限。默认queue:work启动的长驻进程、单任务处理2000条数据的导入逻辑很容易触发限制被杀,进程异常退出后队列会判定任务超时重试,重试次数耗尽后抛出MaxAttemptsExceededException。
  • 调整任务配置适配主机限制
    在ImportJob类中添加公共属性,覆盖框架默认配置:
    // 单任务最大运行时长设置为300秒,建议不超过主机进程存活上限
    public $timeout = 300;
    // 任务失败后重试3次,避免无限重试
    public $tries = 3;
    // 单进程内存占用上限设置为256M,适配共享主机内存限制
    public $memory = 256;
    
    启动队列工作进程时不要使用默认长驻模式,改用一次性执行模式:执行命令为php artisan queue:work --stop-when-empty,该命令会处理完所有待执行任务后自动退出进程,不会长期驻留被主机查杀。
  • 优化导入逻辑降低单任务负载
    当前导入逻辑每一行数据都执行7次查表判断,2000条分块单任务要执行14000次SQL查询,很容易超时。做两个调整:
    • 把ProductImport的chunkSize从2000改为200,缩小单批次处理的数据量,降低单次任务执行时长
    • 实现WithBatchInserts接口,批量写入Product数据减少SQL请求次数;同时提前把已存在的Years、Makers等关联数据全部查出来放到类属性中做内存缓存,不要每行都重复查库
  • 配置定时任务替代手动运行队列
    共享主机无法配置Supervisor守护进程,直接在Hostinger面板的Cron Job中添加定时任务,每分钟执行一次php artisan queue:work --stop-when-empty即可,既可以保证队列任务按时消费,又不会产生长期驻留的进程触发主机查杀规则。
  • 注意不要在共享主机上运行queue:listen,该模式会持续占用进程,一定会被主机系统自动杀掉。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 19:24:22