Laravel 13 任务重试策略完全指南
任务重试是保证异步任务可靠执行的关键机制。本文将深入探讨 Laravel 13 中任务重试的各种策略和最佳实践。
基础重试配置
任务类配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
namespace App\Jobs;
use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue;
class ProcessOrder implements ShouldQueue { use InteractsWithQueue, Queueable;
public int $tries = 3; public int $maxExceptions = 2; public int $timeout = 120; public int $backoff = 60;
public function handle(): void { } }
|
重试次数
1 2 3 4 5 6 7 8 9 10
| class ProcessOrder implements ShouldQueue { public int $tries = 5;
public function tries(): int { return config('queue.jobs.order.tries', 3); } }
|
退避策略
固定退避
1 2 3 4
| class ProcessOrder implements ShouldQueue { public int $backoff = 60; }
|
指数退避
1 2 3 4 5 6 7 8 9 10 11 12
| class ProcessOrder implements ShouldQueue { public array $backoff = [10, 30, 60, 120, 300];
public function backoff(): array { return collect(range(1, 5)) ->map(fn($i) => $i * 60) ->all(); } }
|
自定义退避
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class ProcessOrder implements ShouldQueue { public function backoff(): int|array { $attempt = $this->attempts();
if ($attempt === 1) { return 10; }
if ($attempt === 2) { return 60; }
return min($attempt * 120, 3600); } }
|
重试中间件
指数退避中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?php
namespace App\Jobs\Middleware;
class ExponentialBackoff { protected int $initialDelay = 10; protected int $maxDelay = 3600; protected float $multiplier = 2;
public function __construct( int $initialDelay = 10, int $maxDelay = 3600, float $multiplier = 2 ) { $this->initialDelay = $initialDelay; $this->maxDelay = $maxDelay; $this->multiplier = $multiplier; }
public function handle($job, $next): void { try { $next($job); } catch (\Throwable $e) { $attempt = $job->attempts(); $delay = min( $this->initialDelay * pow($this->multiplier, $attempt - 1), $this->maxDelay );
$job->release($delay); } } }
|
条件重试中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <?php
namespace App\Jobs\Middleware;
use Throwable;
class RetryOnSpecificErrors { protected array $retryableErrors;
public function __construct(array $retryableErrors = []) { $this->retryableErrors = $retryableErrors; }
public function handle($job, $next): void { try { $next($job); } catch (Throwable $e) { if ($this->shouldRetry($e)) { $job->release($this->getDelay($job)); } else { throw $e; } } }
protected function shouldRetry(Throwable $e): bool { foreach ($this->retryableErrors as $errorClass) { if ($e instanceof $errorClass) { return true; } }
return false; }
protected function getDelay($job): int { return $job->backoff()[$job->attempts() - 1] ?? 60; } }
|
限流重试中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <?php
namespace App\Jobs\Middleware;
use Illuminate\Support\Facades\Cache;
class RateLimitedRetry { protected string $key; protected int $maxAttempts; protected int $decaySeconds;
public function __construct( string $key, int $maxAttempts = 5, int $decaySeconds = 60 ) { $this->key = $key; $this->maxAttempts = $maxAttempts; $this->decaySeconds = $decaySeconds; }
public function handle($job, $next): void { $cacheKey = "retry:{$this->key}:" . get_class($job);
$attempts = Cache::get($cacheKey, 0);
if ($attempts >= $this->maxAttempts) { $job->fail(new \Exception('Max retry attempts reached')); return; }
Cache::put($cacheKey, $attempts + 1, $this->decaySeconds);
$next($job); } }
|
重试服务
重试服务类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
class RetryService { protected array $strategies = [];
public function registerStrategy(string $jobClass, array $config): self { $this->strategies[$jobClass] = $config; return $this; }
public function getStrategy(string $jobClass): array { return $this->strategies[$jobClass] ?? [ 'tries' => 3, 'backoff' => [60], 'max_exceptions' => 3, ]; }
public function calculateDelay(string $jobClass, int $attempt): int { $strategy = $this->getStrategy($jobClass); $backoff = $strategy['backoff'] ?? [60];
return $backoff[$attempt - 1] ?? end($backoff); }
public function shouldRetry(string $jobClass, int $attempt, \Throwable $exception): bool { $strategy = $this->getStrategy($jobClass); $maxTries = $strategy['tries'] ?? 3;
if ($attempt >= $maxTries) { return false; }
$retryableExceptions = $strategy['retry_on'] ?? [];
foreach ($retryableExceptions as $exceptionClass) { if ($exception instanceof $exceptionClass) { return true; } }
return true; }
public function logRetry(string $jobClass, int $attempt, \Throwable $exception): void { Log::warning('Job retry', [ 'job' => $jobClass, 'attempt' => $attempt, 'error' => $exception->getMessage(), 'next_delay' => $this->calculateDelay($jobClass, $attempt), ]); } }
|
失败处理
失败回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class ProcessOrder implements ShouldQueue { public function failed(\Throwable $exception): void { Log::error('Order processing failed', [ 'order_id' => $this->order->id, 'error' => $exception->getMessage(), ]);
$this->order->update(['status' => 'failed']);
Notification::route('mail', config('mail.admin_email')) ->notify(new OrderFailed($this->order, $exception)); } }
|
失败后重试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php
namespace App\Services;
use App\Models\FailedJob; use Illuminate\Support\Facades\Artisan;
class FailedJobService { public function retry(int $id): bool { $failedJob = FailedJob::find($id);
if (!$failedJob) { return false; }
Artisan::call('queue:retry', ['id' => $id]);
return true; }
public function retryByQueue(string $queue): int { $count = FailedJob::where('queue', $queue)->count();
FailedJob::where('queue', $queue) ->each(fn($job) => Artisan::call('queue:retry', ['id' => $job->id]));
return $count; }
public function retryByPayload(string $pattern): int { $count = FailedJob::where('payload', 'like', "%{$pattern}%")->count();
FailedJob::where('payload', 'like', "%{$pattern}%") ->each(fn($job) => Artisan::call('queue:retry', ['id' => $job->id]));
return $count; } }
|
手动重试控制
任务内重试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class ProcessOrder implements ShouldQueue { public function handle(): void { try { $this->processOrder(); } catch (TemporaryException $e) { $this->release(60); } catch (PermanentException $e) { $this->fail($e); } }
protected function processOrder(): void { if ($this->order->isLocked()) { $this->release(30); return; }
} }
|
条件重试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class ProcessOrder implements ShouldQueue { public function handle(): void { if ($this->shouldRetry()) { $this->release($this->getDelay()); return; }
$this->process(); }
protected function shouldRetry(): bool { return $this->attempts() < $this->tries && $this->isTemporaryError(); }
protected function getDelay(): int { return $this->backoff()[$this->attempts() - 1] ?? 60; } }
|
重试监控
重试统计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
class RetryMonitor { protected string $key = 'retry:stats';
public function recordRetry(string $jobClass, int $attempt): void { $stats = Cache::get($this->key, []);
$stats[$jobClass] = ($stats[$jobClass] ?? 0) + 1;
Cache::put($this->key, $stats, 3600); }
public function getStats(): array { return Cache::get($this->key, []); }
public function getTopRetries(int $limit = 10): array { $stats = $this->getStats();
arsort($stats);
return array_slice($stats, 0, $limit, true); } }
|
总结
Laravel 13 的任务重试策略提供了:
- 灵活的重试次数配置
- 多种退避策略支持
- 自定义重试中间件
- 条件重试控制
- 失败处理回调
- 重试监控统计
合理配置重试策略可以提高任务的可靠性和系统的稳定性。