Laravel

Laravel 12 队列可靠性实战:Redis、Horizon、幂等、重试与故障恢复

从事务提交后派发、业务幂等、超时退避到 Horizon 运维和失败重放,构建可验证的 Laravel Redis 队列。

TY
Tycho
技术博主
• 2026-09-27 • 22 分钟阅读 • 2 次浏览
Laravel 12 队列可靠性实战:Redis、Horizon、幂等、重试与故障恢复

一、目标与可靠性边界

本章构建 Redis 队列与 Horizon 运维闭环:任务只在事务提交后入队、重复投递不产生重复业务效果、失败任务可观测且能安全重放。队列通常按至少一次处理设计,成功 push 不等于业务已经完成。

可靠性不靠无限重试,而靠事务边界、业务幂等、有限退避、明确超时、失败分类和可控恢复共同完成。

HTTP → MySQL 事务 → after_commit → Redis Queue
                                  ↓
                          Horizon Supervisor
                                  ↓
                幂等 Job → 外部服务 / 数据库
                  ├─ 成功:业务状态落库
                  └─ 失败:退避 → failed_jobs → 审核重放

二、安装 Horizon 与失败任务表

Horizon 只管理 Redis 队列。多个环境共用 Redis 时必须用不同前缀,避免测试任务被生产 worker 消费。安装后先核对连接与 ACL,再启动守护进程。

composer require laravel/horizon
php artisan horizon:install
php artisan queue:failed-table
php artisan migrate
php artisan optimize:clear
QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_QUEUE=default

三、配置事务提交、重试和超时

worker timeout 必须比 retry_after 短数秒,否则任务重新可见时旧进程可能仍在执行,造成并发重复。下例任务最多运行 90 秒,Redis 在 120 秒后才允许重投。

// config/queue.php
'redis' => [
  'driver' => 'redis',
  'connection' => 'default',
  'queue' => env('REDIS_QUEUE', 'default'),
  'retry_after' => 120,
  'block_for' => 5,
  'after_commit' => true,
],
// config/horizon.php
'supervisor-orders' => [
  'connection' => 'redis', 'queue' => ['orders'],
  'balance' => 'auto', 'minProcesses' => 2, 'maxProcesses' => 12,
  'tries' => 5, 'timeout' => 90, 'memory' => 256,
],

四、设计业务幂等任务

任务参数只携带稳定主键,执行时重新查询并锁定状态。数据库状态检查和外部服务幂等键缺一不可。ShouldBeUnique 只能抑制一段时间内的重复入队,不能替代业务幂等。

final class CaptureOrderPayment implements ShouldQueue
{
  use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  public int $tries = 5; public int $timeout = 90;
  public array $backoff = [10, 30, 120, 300];
  public function __construct(public readonly int $orderId) {}
  public function handle(PaymentGateway $gateway): void {
    DB::transaction(function () use ($gateway) {
      $order = Order::query()->lockForUpdate()->findOrFail($this->orderId);
      if ($order->payment_status === 'captured') return;
      $result = $gateway->capture("order-payment:{$order->id}", $order->total_amount);
      $order->update(['payment_status'=>'captured','payment_reference'=>$result->reference]);
    });
  }
}

五、只在数据库提交后派发

afterCommit 保证事务回滚时任务不入队,worker 也不会抢先读取未提交数据。它只覆盖当前数据库事务与派发时机,跨数据库或跨服务一致性仍需要 Outbox 等模式。

DB::transaction(function () use ($request) {
  $order = Order::create([
    'user_id' => $request->user()->id,
    'total_amount' => $request->validated('total_amount'),
    'payment_status' => 'pending',
  ]);
  CaptureOrderPayment::dispatch($order->id)->onQueue('orders')->afterCommit();
});

六、区分可重试和不可重试错误

参数错误与权限错误通常重试无意义,应直接 fail;限流和短暂网络错误按明确延迟 release。不要捕获所有异常后返回成功,否则 Horizon 会把失败误记为完成。

try {
  $this->capture($gateway);
} catch (InvalidPaymentRequest $e) {
  $this->fail($e);
} catch (GatewayRateLimited $e) {
  $this->release(60);
}

public function failed(?Throwable $e): void {
  Log::error('order-payment failed', ['order_id'=>$this->orderId,'type'=>$e?->getMessage()]);
}

七、运行、部署与面板授权

生产使用 systemd、Supervisor 或编排平台守护 Horizon。发布先执行兼容迁移,再优雅终止旧 worker;旧负载与新代码不兼容时增加版本字段,不能直接清空队列。Horizon 面板必须由授权策略保护。

php artisan horizon
php artisan horizon:status
php artisan horizon:supervisors
# 发布新代码后由守护进程拉起新版本
php artisan horizon:terminate

八、测试重复投递与故障恢复

测试必须主动把同一任务执行两次并断言外部调用只发生一次。重放失败任务前确认代码已修复、下游恢复且幂等仍有效;批量 retry all 会放大故障,应按异常和时间窗口小批处理。

$job = new CaptureOrderPayment($order->id);
$job->handle($gateway);
$job->handle($gateway);
$this->assertSame('captured', $order->fresh()->payment_status);
$gateway->shouldHaveReceived('capture')->once();
php artisan test --filter=PaymentJobTest
php artisan queue:failed
php artisan queue:retry <failed-job-uuid>
php artisan queue:forget <failed-job-uuid>

九、监控与容量计划

至少监控队列等待时间、执行时长 P95/P99、失败率、重试率、Redis 内存和 evicted_keys。队列增长时先区分流量增加、单任务变慢和毒消息,再决定扩 worker、拆分队列或限流。

  • 高低优先级分队列,避免批处理阻塞用户任务。
  • 重试要加入退避和抖动,防止下游恢复瞬间雪崩。
  • 日志包含业务主键、job UUID 和尝试次数,但不记录令牌与个人信息。

十、上线与回滚验收

在测试环境验证事务回滚不入队、重复投递仅产生一次业务效果、timeout 小于 retry_after、Horizon 守护与面板授权正确。回滚代码时保持负载向后兼容,必要时暂停消费者而不是删除队列。

  • 部署前保存队列深度和失败任务基线。
  • 部署后观察至少一个业务峰值周期。
  • 失败重放有审批、批次上限和停止阈值。
  • Redis 持久化和恢复策略经过演练。

总结

可靠队列由事务边界、业务幂等、正确超时、有限重试、可观测失败和可控重放共同组成。Horizon 解决 Redis 队列运行管理,但一致性仍要靠状态机、唯一约束和外部幂等键保证。

官方资料与继续学习

TY

Tycho

热爱分享技术知识,帮助开发者成长。

评论 (0)

评论功能当前已关闭
暂无评论,快来抢沙发吧!