本文目录导读:

我来设计一个完善的PHP任务失败重试系统。
基础重试机制设计
重试策略配置类
<?php
namespace App\Retry;
/**
* 重试策略配置
*/
class RetryPolicy
{
/**
* @var int 最大重试次数
*/
private int $maxAttempts = 3;
/**
* @var array 重试延迟(秒),基于指数退避
*/
private array $delaySeconds = [1, 5, 15, 30, 60];
/**
* @var array 需要重试的异常类列表
*/
private array $retryableExceptions = [];
/**
* @var callable|null 自定义重试条件
*/
private $retryConditionCallback = null;
/**
* @var bool 是否启用抖动
*/
private bool $enableJitter = true;
/**
* @var bool 是否记录重试日志
*/
private bool $enableLogging = true;
// Getter和Setter方法...
public function getMaxAttempts(): int
{
return $this->maxAttempts;
}
public function setMaxAttempts(int $maxAttempts): self
{
$this->maxAttempts = $maxAttempts;
return $this;
}
public function getDelaySeconds(): array
{
return $this->delaySeconds;
}
public function setDelaySeconds(array $delaySeconds): self
{
$this->delaySeconds = $delaySeconds;
return $this;
}
public function getRetryableExceptions(): array
{
return $this->retryableExceptions;
}
public function setRetryableExceptions(array $exceptions): self
{
$this->retryableExceptions = $exceptions;
return $this;
}
public function getRetryConditionCallback(): ?callable
{
return $this->retryConditionCallback;
}
public function setRetryConditionCallback(callable $callback): self
{
$this->retryConditionCallback = $callback;
return $this;
}
public function isEnableJitter(): bool
{
return $this->enableJitter;
}
public function setEnableJitter(bool $enableJitter): self
{
$this->enableJitter = $enableJitter;
return $this;
}
public function isEnableLogging(): bool
{
return $this->enableLogging;
}
public function setEnableLogging(bool $enableLogging): self
{
$this->enableLogging = $enableLogging;
return $this;
}
/**
* 获取指定尝试次数的延迟时间(带抖动)
*/
public function getDelayForAttempt(int $attempt): int
{
$index = min($attempt, count($this->delaySeconds) - 1);
$delay = $this->delaySeconds[$index];
if ($this->enableJitter) {
// 添加±20%的随机抖动
$jitter = $delay * 0.2 * (mt_rand() / mt_getrandmax() * 2 - 1);
$delay = max(0, $delay + $jitter);
}
return (int) $delay;
}
}
重试执行器
<?php
namespace App\Retry;
use Psr\Log\LoggerInterface;
use Throwable;
/**
* 重试执行器
*/
class RetryExecutor
{
private LoggerInterface $logger;
private array $retryListeners = [];
public function __construct(?LoggerInterface $logger = null)
{
$this->logger = $logger;
}
/**
* 执行任务并支持重试
*
* @param callable $task 要执行的任务
* @param RetryPolicy|null $policy 重试策略
* @return mixed 任务结果
* @throws Throwable 当所有重试都失败时抛出最后的异常
*/
public function execute(callable $task, ?RetryPolicy $policy = null): mixed
{
$policy = $policy ?? new RetryPolicy();
$attempt = 1;
$lastException = null;
while ($attempt <= $policy->getMaxAttempts()) {
try {
// 执行任务前的回调
$this->triggerEvent('beforeRetry', $attempt, $policy);
// 执行任务
$result = $task();
// 执行成功
$this->triggerEvent('onSuccess', $attempt, $policy);
return $result;
} catch (Throwable $e) {
$lastException = $e;
// 检查是否应该重试
if (!$this->shouldRetry($e, $attempt, $policy)) {
$this->triggerEvent('onFailure', $attempt, $policy, $e);
throw $e;
}
// 计算延迟时间
$delay = $policy->getDelayForAttempt($attempt - 1);
// 记录日志
if ($policy->isEnableLogging() && $this->logger) {
$this->logger->warning(
"Task failed, retrying in {$delay}s. Attempt {$attempt}/{$policy->getMaxAttempts()}",
[
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]
);
}
// 触发重试事件
$this->triggerEvent('onRetry', $attempt, $policy, $e);
// 等待后重试
if ($delay > 0) {
sleep($delay);
}
$attempt++;
}
}
// 所有重试都失败了
$this->triggerEvent('onFailure', $attempt - 1, $policy, $lastException);
throw $lastException;
}
/**
* 判断是否应该重试
*/
private function shouldRetry(Throwable $e, int $attempt, RetryPolicy $policy): bool
{
// 超过最大次数不重试
if ($attempt >= $policy->getMaxAttempts()) {
return false;
}
// 自定义重试条件
$callback = $policy->getRetryConditionCallback();
if ($callback !== null) {
return (bool) call_user_func($callback, $e, $attempt);
}
// 检查异常类型配置
$retryableExceptions = $policy->getRetryableExceptions();
if (!empty($retryableExceptions)) {
foreach ($retryableExceptions as $exceptionClass) {
if ($e instanceof $exceptionClass) {
return true;
}
}
return false;
}
// 默认所有异常都重试
return true;
}
/**
* 注册重试监听器
*/
public function addListener(string $event, callable $callback): void
{
$this->retryListeners[$event][] = $callback;
}
/**
* 触发事件
*/
private function triggerEvent(string $event, int $attempt, RetryPolicy $policy, ?Throwable $e = null): void
{
if (!isset($this->retryListeners[$event])) {
return;
}
foreach ($this->retryListeners[$event] as $callback) {
call_user_func($callback, $attempt, $policy, $e);
}
}
}
持久化任务队列
任务队列类
<?php
namespace App\Queue;
use PDO;
use Redis;
/**
* 持久化任务队列
*/
class TaskQueue
{
private PDO $db;
private ?Redis $redis = null;
private string $table;
public function __construct(PDO $db, string $table = 'tasks', ?Redis $redis = null)
{
$this->db = $db;
$this->table = $table;
$this->redis = $redis;
}
/**
* 创建任务
*/
public function enqueue(string $taskType, array $payload, int $maxRetries = 3): int
{
$sql = "INSERT INTO {$this->table}
(task_type, payload, status, attempts, max_retries, created_at, updated_at)
VALUES (?, ?, 'pending', 0, ?, NOW(), NOW())";
$stmt = $this->db->prepare($sql);
$stmt->execute([
$taskType,
json_encode($payload),
$maxRetries
]);
$taskId = (int) $this->db->lastInsertId();
// 如果有Redis,推送到队列
if ($this->redis) {
$this->redis->lPush('task_queue', $taskId);
}
return $taskId;
}
/**
* 获取待处理任务
*/
public function claimTask(int $workerId): ?array
{
$sql = "SELECT * FROM {$this->table}
WHERE status = 'pending' OR
(status = 'failed' AND attempts < max_retries)
ORDER BY created_at ASC
LIMIT 1 FOR UPDATE SKIP LOCKED";
$stmt = $this->db->prepare($sql);
$stmt->execute();
$task = $stmt->fetch(PDO::FETCH_ASSOC);
if ($task) {
// 标记为执行中
$updateSql = "UPDATE {$this->table}
SET status = 'processing',
worker_id = ?,
started_at = NOW(),
updated_at = NOW()
WHERE id = ?";
$updateStmt = $this->db->prepare($updateSql);
$updateStmt->execute([$workerId, $task['id']]);
return $task;
}
return null;
}
/**
* 标记任务成功
*/
public function markSuccess(int $taskId, array $result = []): void
{
$sql = "UPDATE {$this->table}
SET status = 'success',
result = ?,
completed_at = NOW(),
updated_at = NOW()
WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([json_encode($result), $taskId]);
}
/**
* 标记任务失败(准备重试)
*/
public function markFailed(int $taskId, string $error): bool
{
// 检查任务信息
$task = $this->getTask($taskId);
if (!$task || $task['attempts'] >= $task['max_retries']) {
$this->markDead($taskId, $error);
return false;
}
$attempts = $task['attempts'] + 1;
$sql = "UPDATE {$this->table}
SET status = 'failed',
attempts = ?,
last_error = ?,
next_attempt_at = DATE_ADD(NOW(), INTERVAL ? SECOND),
updated_at = NOW()
WHERE id = ?";
$stmt = $this->db->prepare($sql);
// 计算延迟(指数退避)
$delay = min(300, pow(2, $attempts) * 5); // 5s, 10s, 20s...
$stmt->execute([$attempts, $error, $delay, $taskId]);
return true;
}
/**
* 标记死信任务
*/
public function markDead(int $taskId, string $error): void
{
$sql = "UPDATE {$this->table}
SET status = 'dead',
last_error = ?,
completed_at = NOW(),
updated_at = NOW()
WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$error, $taskId]);
}
/**
* 获取任务
*/
public function getTask(int $taskId): ?array
{
$sql = "SELECT * FROM {$this->table} WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$taskId]);
$task = $stmt->fetch(PDO::FETCH_ASSOC);
return $task ?: null;
}
/**
* 获取统计信息
*/
public function getStats(): array
{
$sql = "SELECT status, COUNT(*) as count
FROM {$this->table}
GROUP BY status";
$stmt = $this->db->query($sql);
$stats = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$stats[$row['status']] = (int) $row['count'];
}
return $stats;
}
}
完整的任务处理器
<?php
namespace App\Queue;
use App\Retry\RetryExecutor;
use App\Retry\RetryPolicy;
use Psr\Log\LoggerInterface;
/**
* 任务处理器基类
*/
abstract class TaskHandler
{
protected TaskQueue $queue;
protected RetryExecutor $retryExecutor;
protected LoggerInterface $logger;
public function __construct(TaskQueue $queue, ?LoggerInterface $logger = null)
{
$this->queue = $queue;
$this->logger = $logger ?? new NullLogger();
$this->retryExecutor = new RetryExecutor($logger);
}
/**
* 执行任务
*/
public function handle(array $task): void
{
$taskId = (int) $task['id'];
$payload = json_decode($task['payload'], true);
try {
// 验证任务数据
if (!$this->validate($payload)) {
$this->queue->markFailed($taskId, 'Validation failed');
return;
}
// 执行任务
$result = $this->process($payload);
// 标记成功
$this->queue->markSuccess($taskId, $result);
} catch (\Exception $e) {
// 记录日志
$this->logger->error("Task {$taskId} failed: " . $e->getMessage());
// 标记失败,决定是否重试
if (!$this->queue->markFailed($taskId, $e->getMessage())) {
// 已达到最大重试次数
$this->onDeadLetter($taskId, $payload, $e);
}
}
}
/**
* 处理任务
*/
abstract protected function process(array $payload): mixed;
/**
* 验证任务数据
*/
protected function validate(array $payload): bool
{
return !empty($payload);
}
/**
* 死信处理(可重写)
*/
protected function onDeadLetter(int $taskId, array $payload, \Exception $e): void
{
$this->logger->error("Task {$taskId} moved to dead letter queue");
}
}
/**
* 示例任务
*/
class OrderTaskHandler extends TaskHandler
{
/**
* 示例:处理订单任务
*/
protected function process(array $payload): mixed
{
// 模拟处理业务
$orderId = $payload['order_id'];
$action = $payload['action'] ?? 'create';
switch ($action) {
case 'create':
return $this->createOrder($orderId);
case 'update':
return $this->updateOrder($orderId, $payload['data'] ?? []);
default:
throw new \InvalidArgumentException("Unknown action: {$action}");
}
}
private function createOrder(int $orderId): array
{
// 模拟业务逻辑
if ($orderId % 5 == 0) {
throw new \RuntimeException("Database connection failed");
}
return ['status' => 'created', 'order_id' => $orderId];
}
private function updateOrder(int $orderId, array $data): array
{
return ['status' => 'updated', 'order_id' => $orderId, 'data' => $data];
}
}
独立重试工具类
<?php
namespace App\Retry;
use Closure;
use Throwable;
/**
* 独立的重试工具类
*/
class Retry
{
/**
* 带重试的闭包执行
*
* @param Closure $fn 需要重试的函数
* @param array $options 配置选项
* @return mixed
*/
public static function withRetry(Closure $fn, array $options = []): mixed
{
$attempts = $options['attempts'] ?? 3;
$delay = $options['delay'] ?? 1; // 基础延迟秒数
$multiplier = $options['multiplier'] ?? 2; // 延迟倍数
$maxDelay = $options['max_delay'] ?? 60; // 最大延迟
$jitter = $options['jitter'] ?? false; // 是否添加抖动
$onRetry = $options['on_retry'] ?? null; // 重试回调
$retryCondition = $options['retry_condition'] ?? null; // 重试条件
$attempt = 1;
$lastException = null;
while (true) {
try {
return $fn();
} catch (Throwable $e) {
$lastException = $e;
// 检查是否需要重试
if ($attempt >= $attempts) {
break;
}
// 检查重试条件(如果有)
if ($retryCondition && !call_user_func($retryCondition, $e)) {
break;
}
// 计算延迟时间
$waitTime = min($maxDelay, $delay * pow($multiplier, $attempt - 1));
if ($jitter) {
// 添加随机抖动(±20%)
$waitTime *= (1 + (mt_rand() / mt_getrandmax() - 0.5) * 0.2);
}
// 重试回调
if ($onRetry) {
call_user_func($onRetry, $attempt, $waitTime, $e);
}
sleep(max(0, (int) $waitTime));
$attempt++;
}
}
throw $lastException;
}
/**
* 异步重试辅助类
*/
public static function async(callable $fn, array $options = []): mixed
{
// 示例:在异步环境中使用
return self::withRetry($fn, $options);
}
/**
* 并发重试多个任务
*/
public static function all(array $tasks, array $options = []): array
{
$results = [];
$errors = [];
foreach ($tasks as $key => $task) {
try {
$results[$key] = self::withRetry($task, $options);
} catch (Throwable $e) {
$errors[$key] = $e;
}
}
if (!empty($errors)) {
throw new \RuntimeException(
'Multiple tasks failed: ' . implode(', ', array_keys($errors))
);
}
return $results;
}
}
使用示例
<?php
// 1. 使用基础重试类
use App\Retry\RetryExecutor;
use App\Retry\RetryPolicy;
$retryPolicy = new RetryPolicy();
$retryPolicy->setMaxAttempts(3)
->setDelaySeconds([1, 5, 15])
->setRetryableExceptions([
\PDOException::class,
\RedisException::class,
])
->setEnableJitter(true)
->setEnableLogging(true);
$executor = new RetryExecutor($logger);
try {
$result = $executor->execute(function () {
// 你的业务逻辑
return performBusinessOperation();
}, $retryPolicy);
} catch (\Exception $e) {
echo "任务最终失败: " . $e->getMessage();
}
// 2. 使用简洁重试方法
use App\Retry\Retry;
$result = Retry::withRetry(function () {
// 业务代码
return $this->callExternalApi();
}, [
'attempts' => 5,
'delay' => 1,
'multiplier' => 2,
'max_delay' => 30,
'jitter' => true,
'retry_condition' => function ($exception) {
// 只重试特定异常
return $exception instanceof \RuntimeException;
},
'on_retry' => function ($attempt, $delay, $exception) {
Log::warning("第{$attempt}次重试,等待{$delay}秒", [
'error' => $exception->getMessage()
]);
}
]);
// 3. 使用任务队列
use App\Queue\TaskQueue;
$queue = new TaskQueue($pdo, 'tasks', $redis);
// 添加任务
$taskId = $queue->enqueue(
'order.process',
['order_id' => 123],
3 // 最大重试3次
);
// 处理任务(worker进程)
$task = $queue->claimTask($workerId);
// 根据任务类型分发处理
$handlerMap = [
'order.process' => new OrderTaskHandler($queue, $logger),
'email.send' => new EmailTaskHandler($queue, $logger),
];
if ($task && isset($handlerMap[$task['task_type']])) {
$handler = $handlerMap[$task['task_type']];
$handler->handle($task);
}
数据库表结构
-- 任务表
CREATE TABLE IF NOT EXISTS `tasks` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`task_type` VARCHAR(100) NOT NULL COMMENT '任务类型',
`payload` JSON NOT NULL COMMENT '任务数据',
`status` ENUM('pending', 'processing', 'success', 'failed', 'dead')
DEFAULT 'pending' COMMENT '任务状态',
`attempts` INT UNSIGNED DEFAULT 0 COMMENT '已尝试次数',
`max_retries` INT UNSIGNED DEFAULT 3 COMMENT '最大重试次数',
`worker_id` VARCHAR(64) DEFAULT NULL COMMENT '处理worker',
`result` JSON DEFAULT NULL COMMENT '成功结果',
`last_error` TEXT DEFAULT NULL COMMENT '最后错误信息',
`next_attempt_at` DATETIME DEFAULT NULL COMMENT '下次重试时间',
`started_at` DATETIME DEFAULT NULL COMMENT '开始时间',
`completed_at` DATETIME DEFAULT NULL COMMENT '完成时间',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
INDEX `idx_status_created` (`status`, `created_at`),
INDEX `idx_next_attempt` (`status`, `next_attempt_at`),
INDEX `idx_worker` (`worker_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
最佳实践建议
错误分类处理
class RetryableException extends \Exception {}
class NonRetryableException extends \Exception {}
// 使用方式
if ($e instanceof NonRetryableException) {
// 不重试,直接失败
} else {
// 可以进行重试
}
监控和告警
// 添加监控指标
class RetryMonitor
{
public static function recordRetry(string $taskType, int $attempt): void
{
// 发送到监控系统(如Prometheus)
Metrics::increment("task.retry", [
'type' => $taskType,
'attempt' => $attempt
]);
}
public static function recordDeadLetter(string $taskType): void
{
Metrics::increment("task.dead_letter", [
'type' => $taskType
]);
}
}
这个设计提供了完整的任务重试机制,包括:
- 灵活的配置:支持自定义重试次数、延迟策略、重试条件
- 持久化存储:数据库存储任务状态,支持分布式处理
- 指数退避:智能延迟算法避免雪崩
- 异常分类:区分可重试和不可重试异常
- 监控集成:支持各种事件监听和指标收集
- 线程安全:使用数据库锁和Redis避免并发冲突