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; // 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 的任务重试策略提供了:

  • 灵活的重试次数配置
  • 多种退避策略支持
  • 自定义重试中间件
  • 条件重试控制
  • 失败处理回调
  • 重试监控统计

合理配置重试策略可以提高任务的可靠性和系统的稳定性。