应通过failed方法中判断$exception::class类型区分业务与系统异常,分别归档至不同表,并在Horizon中注册自定义FailedJobProvider以支持分类查看。

队列任务失败后怎么区分是业务异常还是系统异常
Laravel 默认把所有失败任务都塞进 failed_jobs 表,不区分错误类型,查问题时得手动翻堆栈。真正有用的是把 ValidationException、ModelNotFoundException 这类可预期的业务异常和 ConnectionException、TimeoutException 这类系统级故障分开归档。
关键不是改 Laravel 源码,而是利用 failed 方法的参数——它会传入 $job 和 $exception,你可以直接对 $exception::class 做判断:
public function failed($job, $exception)
{
$type = get_class($exception);
if (in_array($type, [
'Illuminate\Validation\ValidationException',
'App\Exceptions\BusinessRuleViolationException'
])) {
DB::table('business_failed_jobs')->insert([...]);
return;
}
// 其他归到系统异常表
DB::table('system_failed_jobs')->insert([...]);
}
- 别用
$exception->getMessage()做字符串匹配,不稳定,容易漏或误判 - 注意
get_class()返回的是完整命名空间,比如"Illuminate\Database\QueryException",别漏了前缀 - 如果用了 Horizon,记得在
config/horizon.php的failed配置里也指向这个自定义方法,否则 Horizon 会绕过你的逻辑
Laravel 9+ 的 retryUntil 和异常类型联动失效怎么办
很多人以为给任务加了 retryUntil() 就能自动按异常类型决定重试次数,其实不会。Laravel 的重试机制只看是否抛出异常,不关心类型;retryUntil 只控制“最多试到什么时候”,不是“遇到某类异常就停”。
真要实现“验证失败不重试,连接超时重试 3 次”,得自己写重试逻辑:
public function handle()
{
try {
$this->doActualWork();
} catch (\Illuminate\Validation\ValidationException $e) {
// 业务校验失败,直接标记失败,不重试
throw $e;
} catch (\Illuminate\Database\QueryException $e) {
// 数据库异常,主动触发重试(需配合 $tries 或 retryAfter)
$this->release(60); // 60 秒后重试
return;
}
}
-
$this->release()是关键,它相当于手动投递回队列,比依赖框架重试更可控 - 别在
catch里静默吞掉异常又不调release()或fail(),会导致任务“消失”且无记录 - 如果用了 Redis 队列,
release()会走redis->lPush,确保你的 Redis 连接在异常后仍可用,否则可能卡住
自定义失败处理时,failed 方法里访问数据库报错怎么办
常见现象:任务失败后,failed 方法里执行 DB::table(...)->insert(...) 报 "No application encryption key has been specified" 或 "Database connection not configured" —— 这是因为 Laravel 在失败回调里未必完成完整启动流程,尤其用 php artisan queue:work --once 手动触发时。
- 优先用原生 PDO 或
DB::connection('mysql')->insert(),绕过 Eloquent 和配置解析层 - 避免在
failed里调用任何依赖服务容器绑定的类(比如Notification::route()),它们可能没加载 - 最稳的方式是把归档逻辑扔进另一个队列任务里异步执行:
FailureArchiverJob::dispatch($job, $exception)->onQueue('logs'),主流程不阻塞也不依赖环境
Horizon 控制台里看不到按类型分类的失败记录
Horizon 默认只读 failed_jobs 表,你往 business_failed_jobs 插数据,它根本不知道。想让它显示,必须改两个地方:
- 在
config/horizon.php的environments下,把'failed' => ['database', 'redis']改成'failed' => ['database', 'redis', 'business', 'system'](假设你注册了这两个自定义驱动) - 为每个新表写一个
FailedJobProvider实现,继承Illuminate\Queue\Failed\FailedJobProviderInterface,重点重写get()和all()方法,返回对应表的数据 - 然后在
App\Providers\AppServiceProvider的boot()里用$this->app['queue.failer']->extend('business', ...)注册进去
这一步最容易被跳过:光建表、光插数据,不告诉 Horizon,它永远只刷默认表。而且 Horizon 的前端 JS 是硬编码读 /horizon/api/failed 接口,那个接口底层就是调你注册的 provider。


















