> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 队列任务的执行控制

> 介绍如何使用 ShouldBeUnique、ShouldBeUniqueUntilProcessing 和 DebounceFor 控制队列任务的重复执行与连续派发。

## 概述

Laravel 的队列功能提供了两种执行控制：任务的**去重（Unique）**和**防抖（Debounce）**。两者都是为了在同一任务被多次派发时避免无谓的执行，但行为有所不同。

| 功能                      | 接口 / 属性                         | 目的                  |
| ----------------------- | ------------------------------- | ------------------- |
| Unique Jobs             | `ShouldBeUnique`                | 保持队列中同一任务仅存在一个      |
| Unique Until Processing | `ShouldBeUniqueUntilProcessing` | 仅在处理开始前保持唯一约束       |
| Debounced Jobs          | `#[DebounceFor]`                | 在短时间内连续派发时，仅执行最新的一个 |

<Warning>
  Unique Jobs 与 Debounced Jobs **互斥**。请不要在使用 `DebounceFor` 属性的任务上实现 `ShouldBeUnique`。
</Warning>

***

## Unique Jobs — `ShouldBeUnique`

只要同一任务存在于队列中，其他派发都会被忽略。

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // 无需额外方法
}
```

当 `UpdateSearchIndex` 位于队列中（或正在处理中）时，尝试派发同一任务会被忽略。

### 通过键细化唯一约束 — `UniqueFor` + `uniqueId()`

即使是同一任务类，若希望将"更新商品 A"和"更新商品 B"视为不同任务，可通过 `uniqueId()` 方法定义键。

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\UniqueFor;

#[UniqueFor(3600)] // 1 小时后自动释放锁
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public function __construct(public readonly int $productId)
    {
    }

    public function uniqueId(): string
    {
        return (string) $this->productId;
    }
}
```

* `uniqueId()` 返回的值将作为缓存锁的键。
* 指定 `#[UniqueFor(秒数)]` 后，经过该秒数后锁将自动释放（当任务未被处理时的故障保护）。

### 指定缓存驱动 — `uniqueVia()`

若希望使用默认缓存驱动以外的驱动，可实现 `uniqueVia()`。

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function uniqueVia(): Repository
{
    return Cache::driver('redis');
}
```

<Info>
  Unique Jobs 需要支持原子锁的缓存驱动（`redis`、`database`、`memcached`、`dynamodb`、`file`、`array`）。
</Info>

***

## `ShouldBeUnique` vs `ShouldBeUniqueUntilProcessing`

`ShouldBeUnique` 的锁会**保持到任务完成或达到重试上限**。这在某些场景下会成为问题。

**示例：** 队列中有一个 `UpdateSearchIndex(product_id: 42)`，希望在 Worker 开始处理后立即再次派发同一任务。使用 `ShouldBeUnique` 时，在处理完成前第二个任务不会入队。

此时应使用 `ShouldBeUniqueUntilProcessing`。由于锁在**处理开始前**就被释放，因此 Worker 取出任务的瞬间即可派发下一个任务。

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
    // ...
}
```

```mermaid theme={null}
sequenceDiagram
    participant D as 派发者
    participant Q as 队列
    participant W as Worker

    D->>Q: dispatch() — 获取锁
    D->>Q: dispatch() — 已有锁 → 忽略

    note over Q,W: ShouldBeUnique 的情况
    W->>Q: 取出任务
    W->>W: 处理中（持有锁）
    D->>Q: dispatch() — 已有锁 → 忽略
    W->>W: 处理完成 — 释放锁
    D->>Q: dispatch() — 此时才可入队

    note over Q,W: ShouldBeUniqueUntilProcessing 的情况
    W->>Q: 取出任务 — 释放锁
    D->>Q: dispatch() — 无锁 → 可入队
    W->>W: 处理中
```

### 对比总结

|           | `ShouldBeUnique` | `ShouldBeUniqueUntilProcessing` |
| --------- | ---------------- | ------------------------------- |
| 锁释放时机     | 处理完成 / 失败后       | 处理开始前                           |
| 处理期间的重复派发 | 忽略               | 可入队                             |
| 使用场景      | 完全防止并发执行         | 处理后立即想派发下一个任务                   |

***

## Debounced Jobs — `#[DebounceFor]`

<Info>
  `DebounceFor` 属性是 Laravel 13 新增的功能。
</Info>

当同一任务在短时间内被大量派发时，**仅执行最后一次派发的任务**。这与 Web 前端的防抖思路相同。

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30)] // 30 秒内的再次派发被忽略（仅执行最新一个）
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly int $productId)
    {
    }

    public function debounceId(): string
    {
        return (string) $this->productId;
    }
}
```

* 由 `debounceId()` 返回值来识别任务（每个商品 ID 会应用独立的防抖）。
* 即使 30 秒内以相同 `productId` 派发 10 次，也只会执行最后一次。

### `maxWait` — 最大等待时间上限

对于频繁更新的数据，防抖可能会不断持续从而导致任务永远无法执行。可以通过 `maxWait` 设置最大延迟时间。

```php theme={null}
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;
    // ...
}
```

在此示例中，从首次派发算起，最迟 120 秒后必定会执行（即使 30 秒防抖持续也会在 120 秒时超时执行）。

### 指定缓存驱动 — `debounceVia()`

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function debounceVia(): Repository
{
    return Cache::driver('redis');
}
```

### `JobDebounced` 事件

被后续派发覆盖的任务会派发 `Illuminate\Queue\Events\JobDebounced` 事件并从队列中删除。通过监听该事件，可以对被防抖的任务进行跟踪和监控。

***

## 应该选择哪一个

```mermaid theme={null}
flowchart TD
    A["可能会多次派发相同任务"] --> B{"短时间内连续派发<br>只想执行最新一个？"}
    B -->|是| C["#[DebounceFor]"]
    B -->|否| D{"希望队列中<br>只存在一个？"}
    D -->|是| E{"处理中也要防止重复？"}
    E -->|是| F["ShouldBeUnique"]
    E -->|否| G["ShouldBeUniqueUntilProcessing"]
    D -->|否| H["普通任务"]
```

| 使用场景                 | 推荐                              |
| -------------------- | ------------------------------- |
| 队列中有 2 个以上相同处理没有意义   | `ShouldBeUnique`                |
| 希望连处理中也防止并行执行        | `ShouldBeUnique`                |
| Worker 取出后希望立即入队下一个  | `ShouldBeUniqueUntilProcessing` |
| 用户连按保存按钮也只想执行一次      | `#[DebounceFor]`                |
| 每次模型更新都重建搜索索引（大量更新时） | `#[DebounceFor]` + `maxWait`    |

***

## 内部实现

### Unique Jobs 的锁机制

当派发 `ShouldBeUnique` 任务时，Laravel 会在内部获取缓存的[原子锁](/zh-CN/cache#原子操作锁)。锁键的格式如下：

```
laravel_unique_job:{任务类名}:{uniqueId()}
```

如果无法获取锁（已被其他任务持有），任务不会被添加到队列中。

### Debounced Jobs 的实现

`DebounceFor` 内部使用管理"防抖窗口"的缓存条目。每当新的派发到来时：

1. 从队列中删除已存在的任务（派发 `JobDebounced` 事件）
2. 将新任务添加到队列（带有防抖秒数的延迟）
3. 重置缓存中的计时器

若指定了 `maxWait`，也会记录首次派发的时间戳，并防止从该时刻起防抖超过 `maxWait` 秒。

***

## 参考链接

* [Laravel 官方文档 — Unique Jobs](https://laravel.com/docs/queues#unique-jobs)
* [Laravel 官方文档 — Debounced Jobs](https://laravel.com/docs/queues#debounced-jobs)
* [`Illuminate\Contracts\Queue\ShouldBeUnique`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUnique.php)
* [`Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUniqueUntilProcessing.php)
* [`Illuminate\Queue\Attributes\DebounceFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/DebounceFor.php)
* [`Illuminate\Queue\Attributes\UniqueFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/UniqueFor.php)


## Related topics

- [任务调度](/zh-CN/scheduling.md)
- [Laravel Nightwatch 入门](/zh-CN/blog/nightwatch-introduction.md)
- [Laravel Telescope](/zh-CN/telescope.md)
- [Context（上下文）](/zh-CN/context.md)
- [控制器的 PHP 属性](/zh-CN/advanced/controller-attributes.md)
