PHP项目Laravel任务调度失败重试机制

wen PHP项目 4

本文目录导读:

PHP项目Laravel任务调度失败重试机制

  1. 使用队列的重试机制
  2. 调度任务中的重试
  3. 使用Redis锁实现分布式重试
  4. 使用第三方包
  5. 日志和监控
  6. 最佳实践
  7. 注意事项

在Laravel中实现任务调度的失败重试机制,有以下几种方案:

使用队列的重试机制

基础配置

// config/queue.php
'connections' => [
    'database' => [
        'driver' => 'database',
        'table' => 'jobs',
        'queue' => 'default',
        'retry_after' => 90, // 任务失败后重试的延迟时间(秒)
    ],
],

定义可重试任务

// app/Jobs/ProcessReport.php
class ProcessReport implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    public $tries = 3; // 最大重试次数
    public $backoff = [60, 300, 600]; // 每次重试的延迟时间(秒)
    public $timeout = 120; // 任务超时时间
    public $maxExceptions = 3; // 最大异常次数
    public function handle()
    {
        // 业务逻辑
        try {
            // 处理任务
        } catch (\Exception $e) {
            // 记录日志
            Log::error('任务失败', [
                'error' => $e->getMessage(),
                'attempt' => $this->attempts()
            ]);
            throw $e;
        }
    }
    // 失败后的回调
    public function failed(\Throwable $exception)
    {
        Log::error('任务最终失败', [
            'message' => $exception->getMessage(),
            'job_id' => $this->job->getJobId()
        ]);
    }
}

调度任务中的重试

使用调度器配置

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    $schedule->command('report:generate')
        ->daily()
        ->withoutOverlapping() // 防止任务重叠
        ->onOneServer() // 只在单服务器执行
        ->runInBackground() // 后台运行
        ->onFailure(function () {
            // 失败时发送通知
            Notification::send($admins, new TaskFailedNotification());
        });
}

手动重试机制

// app/Console/Commands/GenerateReport.php
class GenerateReport extends Command
{
    protected $signature = 'report:generate';
    protected $maxRetries = 3;
    protected $retryDelay = 60; // 秒
    public function handle()
    {
        $attempt = 1;
        while ($attempt <= $this->maxRetries) {
            try {
                $this->processData();
                $this->info('报表生成成功');
                return 0;
            } catch (\Exception $e) {
                Log::error("第{$attempt}次尝试失败", [
                    'error' => $e->getMessage()
                ]);
                if ($attempt < $this->maxRetries) {
                    $this->info("等待 {$this->retryDelay} 秒后重试...");
                    sleep($this->retryDelay);
                    $attempt++;
                } else {
                    $this->error('所有重试都失败了');
                    return 1;
                }
            }
        }
    }
}

使用Redis锁实现分布式重试

// app/Console/Commands/ProcessData.php
class ProcessData extends Command
{
    protected $signature = 'data:process';
    public function handle()
    {
        $lock = Cache::lock('data-processing', 120);
        try {
            if ($lock->get()) {
                // 检查是否有限制
                $retryCount = Cache::get('data-process-retry', 0);
                if ($retryCount >= 3) {
                    $this->error('超过最大重试次数');
                    return 1;
                }
                $this->processData();
                // 成功,清除重试计数
                Cache::forget('data-process-retry');
                return 0;
            }
        } catch (\Exception $e) {
            // 失败,递增重试计数
            $retryCount = Cache::increment('data-process-retry');
            Cache::put('data-process-retry-time', now(), 3600);
            Log::error("处理失败,第{$retryCount}次重试");
            // 延迟重试
            $this->scheduleRetry($retryCount);
        } finally {
            $lock->release();
        }
    }
    private function scheduleRetry($retryCount)
    {
        $delay = [5, 15, 30][$retryCount - 1] ?? 60;
        // 通过队列延迟执行
        ProcessDataJob::dispatch()->delay(now()->addSeconds($delay));
        // 或者通过调度器
        $this->line("将在 {$delay} 秒后重试");
        Event::dispatch('data.process.retry', new DataProcessRetryEvent($retryCount));
    }
}

使用第三方包

supervisord 配置

[program:laravel-scheduler]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/project/artisan schedule:work
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/path/to/project/storage/logs/scheduler.log
stopwaitsecs=3600

Laravel Horizon

// config/horizon.php
'defaults' => [
    'supervisor-1' => [
        'connection' => 'redis',
        'queue' => ['default'],
        'balance' => 'auto',
        'processes' => 1,
        'tries' => 3,
        'timeout' => 60,
        'retry_after' => 90,
    ],
],

日志和监控

// app/Http/Middleware/TaskRetryMiddleware.php
class TaskRetryMiddleware
{
    public function handle($job, $next)
    {
        $attempt = $job->attempts();
        try {
            $response = $next($job);
            // 记录成功日志
            Log::info('任务执行成功', [
                'job' => get_class($job),
                'attempts' => $attempt
            ]);
            return $response;
        } catch (\Exception $e) {
            // 记录失败日志
            Log::error('任务执行失败', [
                'job' => get_class($job),
                'attempts' => $attempt,
                'error' => $e->getMessage()
            ]);
            // 根据失败类型决定是否重试
            if ($this->shouldRetry($e)) {
                $job->release(60); // 延迟60秒重试
            } else {
                throw $e;
            }
        }
    }
    private function shouldRetry($exception)
    {
        // 不重试某些特定异常
        return !($exception instanceof \DomainException);
    }
}

最佳实践

// app/Console/Commands/ScheduleTask.php
class ScheduleTask extends Command
{
    protected $signature = 'scheduler:retry-task';
    public function handle()
    {
        $maxRetries = 3;
        $retryCount = 0;
        do {
            try {
                $this->performTask();
                $this->info('任务成功');
                break;
            } catch (\Exception $e) {
                $retryCount++;
                $this->error("第 {$retryCount} 次失败: " . $e->getMessage());
                if ($retryCount >= $maxRetries) {
                    $this->notifyFailure($e);
                    return 1;
                }
                $this->line('等待重试...');
                sleep(pow(2, $retryCount) * 10); // 指数退避
            }
        } while ($retryCount < $maxRetries);
        return 0;
    }
}

注意事项

  1. 幂等性:确保任务可重复执行
  2. 事务安全:处理数据库事务回滚
  3. 监控告警:重试失败必须通知相关人员
  4. 资源控制:防止无限重试耗尽资源

选择合适的方案取决于你的具体需求:简单的任务可用内置重试,重要任务建议用队列+失败记录机制,生产环境建议配置完善的监控和告警系统。

抱歉,评论功能暂时关闭!