本文目录导读:

在 Laravel 中实现事件监听失败重试机制,有几种方案:
使用队列 + 重试机制
配置队列重试
// config/queue.php
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90, // 重试延迟时间
'block_for' => null,
'after_commit' => null,
],
],
定义事件和监听器
// app/Events/OrderCreated.php
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderCreated
{
use Dispatchable, SerializesModels;
public $order;
public function __construct($order)
{
$this->order = $order;
}
}
// app/Listeners/SendOrderNotification.php
namespace App\Listeners;
use App\Events\OrderCreated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendOrderNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* 失败重试次数
*/
public $tries = 3;
/**
* 最大执行时间(秒)
*/
public $timeout = 60;
/**
* 重试延迟时间(秒)
*/
public $backoff = [2, 5, 10]; // 分别对应第1、2、3次重试的延迟
public function handle(OrderCreated $event)
{
// 业务逻辑
try {
// 发送通知
$result = $this->sendNotification($event->order);
if (!$result) {
throw new \Exception('通知发送失败');
}
} catch (\Exception $e) {
// 记录日志
\Log::error('通知发送失败', [
'order_id' => $event->order->id,
'error' => $e->getMessage()
]);
// 重新抛出异常以触发重试
throw $e;
}
}
private function sendNotification($order)
{
// 实际发送逻辑
return true;
}
/**
* 失败处理方法
*/
public function failed(OrderCreated $event, $exception)
{
\Log::critical('通知发送最终失败', [
'order_id' => $event->order->id,
'error' => $exception->getMessage()
]);
}
}
自定义重试中间件
// app/Queue/Middleware/CustomRetryMiddleware.php
namespace App\Queue\Middleware;
use Closure;
use Illuminate\Queue\Jobs\Job;
use Illuminate\Queue\Middleware\RateLimited;
class CustomRetryMiddleware
{
/**
* @param Job $job
* @param Closure $next
* @return mixed
*/
public function handle($job, $next)
{
try {
return $next($job);
} catch (\Throwable $e) {
// 判断是否需要重试
if ($this->shouldRetry($job, $e)) {
// 记录重试日志
\Log::warning('任务执行失败,准备重试', [
'job' => get_class($job),
'attempt' => $job->attempts(),
'error' => $e->getMessage()
]);
// 延迟重试
$job->release(10); // 延迟10秒后重试
} else {
throw $e;
}
}
}
private function shouldRetry($job, $exception)
{
// 重试次数限制
$maxAttempts = 3;
if ($job->attempts() >= $maxAttempts) {
\Log::error('任务达到最大重试次数', [
'job' => get_class($job),
'attempts' => $job->attempts()
]);
return false;
}
// 可以基于异常类型判断是否重试
if ($exception instanceof \Illuminate\Database\QueryException) {
// 数据库异常,不重试
return false;
}
return true;
}
}
事件系统结合缓存实现重试
// app/Listeners/FailedEventListener.php
namespace App\Listeners;
use Illuminate\Cache\RedisStore;
use Illuminate\Support\Facades\Cache;
class FailedEventListener
{
/**
* 处理事件
*/
public function handle($event)
{
// 存储处理状态
$cacheKey = 'event_retry_' . get_class($event) . '_' . $event->id;
if (!Cache::has($cacheKey)) {
Cache::put($cacheKey, ['status' => 'processing'], 3600);
}
try {
// 业务逻辑
$result = $this->processEvent($event);
// 处理成功,存储状态
Cache::put($cacheKey, ['status' => 'success'], 3600);
return $result;
} catch (\Exception $e) {
// 记录失败信息
$failed = Cache::get($cacheKey);
$attempts = isset($failed['attempts']) ? $failed['attempts'] : 0;
$attempts++;
if ($attempts < 3) {
// 设置下次重试时间
$retryTime = now()->addMinutes(5);
Cache::put($cacheKey, [
'status' => 'failed',
'attempts' => $attempts,
'next_retry' => $retryTime,
'last_error' => $e->getMessage()
], 3600);
// 调度重试任务
$this->scheduleRetry(get_class($event), $event->id, $retryTime);
} else {
\Log::critical('事件处理多次失败', [
'event' => get_class($event),
'id' => $event->id,
'attempts' => $attempts
]);
}
throw $e;
}
}
private function scheduleRetry($eventClass, $eventId, $retryTime)
{
// 使用 Laravel 的调度器
$schedule = app(\Illuminate\Console\Scheduling\Schedule::class);
$schedule->call(function () use ($eventClass, $eventId) {
$event = $eventClass::find($eventId);
if ($event) {
event(new $eventClass($event));
}
})->at($retryTime->format('Y-m-d H:i'));
}
}
使用第三方包
安装 Laravel Horizon 支持队列监控
composer require laravel/horizon php artisan horizon:install php artisan migrate
Horizon 配置
// config/horizon.php
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'processes' => 1,
'tries' => 3, // 重试次数
'timeout' => 60, // 超时时间
'nice' => 0,
],
],
完整的失败重试系统
// app/Services/EventRetryService.php
namespace App\Services;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;
class EventRetryService
{
protected $maxRetries = 3;
protected $retryDelays = [1, 5, 15]; // 分钟
/**
* 注册事件监听器
*/
public function registerListeners(array $events)
{
foreach ($events as $eventClass => $listenerClass) {
\Event::listen($eventClass, $listenerClass);
}
}
/**
* 处理事件失败
*/
public function handleFailure($event, $exception, $listenerClass)
{
$eventId = $this->getEventId($event);
$retryKey = "event_retry:{$eventId}";
// 获取当前重试次数
$attempts = (int) Redis::hget($retryKey, 'attempts');
$attempts++;
// 记录失败信息
Redis::hset($retryKey, 'attempts', $attempts);
Redis::hset($retryKey, 'last_error', $exception->getMessage());
Redis::hset($retryKey, 'listener', $listenerClass);
// 判断是否继续重试
if ($attempts <= $this->maxRetries) {
$this->scheduleRetry($event, $attempts);
} else {
$this->markAsPermanentFailure($eventId);
}
// 记录日志
Log::error('事件处理失败', [
'event' => get_class($event),
'listener' => $listenerClass,
'attempts' => $attempts,
'error' => $exception->getMessage()
]);
}
/**
* 调度重试
*/
protected function scheduleRetry($event, $attempts)
{
$delay = $this->retryDelays[$attempts - 1] ?? 30; // 分钟
// 使用队列延迟执行
\Queue::later(
now()->addMinutes($delay),
new \App\Jobs\RetryEventJob($event)
);
// 或者使用 Redis 的延迟队列
Redis::zadd('event_retry_queue',
time() + ($delay * 60),
serialize($event)
);
}
/**
* 标记永久失败
*/
protected function markAsPermanentFailure($eventId)
{
$key = "event_retry:{$eventId}";
Redis::hset($key, 'status', 'permanent_failure');
Log::critical("事件处理永久失败,事件ID: {$eventId}");
}
}
// app/Jobs/RetryEventJob.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class RetryEventJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $event;
public function __construct($event)
{
$this->event = $event;
}
public function handle()
{
try {
// 重新触发事件
event($this->event);
} catch (\Exception $e) {
// 如果还是失败,记录日志
Log::error('重试事件失败', [
'event' => get_class($this->event),
'error' => $e->getMessage()
]);
// 可以考虑再次触发重试
if ($this->attempts() < 3) {
$this->release(60 * 5); // 5分钟后重试
}
}
}
}
最佳实践建议
- 设置合理的重试次数:不要无限重试,一般 3-5 次即可
- 使用指数退避:逐步增加重试间隔
- 区分错误类型:
- 可重试错误(网络问题、外部服务暂时不可用)
- 不可重试错误(数据验证错误、权限问题)
- 监控通知:设置告警机制
- 记录完整日志:便于排查问题
// 注册事件时指定重试策略
Event::listen(
OrderCreated::class,
[SendOrderNotification::class, 'handle'],
10 // 优先级
);
// 监听器类定义重试参数
class SendOrderNotification implements ShouldQueue
{
public $tries = 3;
public $timeout = 60;
public $backoff = [1, 5, 10];
public $maxExceptions = 3;
}
选择哪种方案取决于你的具体需求:
- 简单场景:使用方案一(队列配置即可)
- 复杂业务:使用方案二(自定义中间件)
- 需要监控:使用方案四(Horizon)
- 分布式系统:使用方案三(缓存/Redis)