shouldbeunique 是 laravel 9.2+ 原生队列唯一性保障机制,任务入队前自动生成指纹并查缓存决定是否丢弃,原子性由框架保障,避免手动 redis 锁的过期遗漏、类型错误与作用域混淆等问题。

直接用 ShouldBeUnique 接口,别自己手写 Redis 锁或在 handle() 里加判断——Laravel 9.2+ 已原生支持,稳定、可测、不踩坑。
ShouldBeUnique 是什么,为什么比手动锁更可靠
它不是装饰器,也不是中间件,而是 Laravel 队列调度层的原生钩子:任务进队前就生成指纹、查缓存、决定是否丢弃。整个过程原子性由框架保障,不依赖你手写的 Cache::add() 或 Redis::set(..., 'NX') ——后者一旦漏掉 EX 参数或异常未释放锁,就会卡死。
常见错误现象:Cache::store('redis')->add($key, true, 60) 忘设过期时间,锁永久存在;try/finally 里 forget() 被网络中断跳过,锁残留;多个 Job 类共用同一 key 前缀导致误删。
-
ShouldBeUnique的指纹默认是类名 + 序列化后的属性,自动避开了参数拼接出错风险 - 锁生命周期由 Laravel 统一管理,失败时自动清理,不依赖你写 finally
- 支持延迟任务(
delay()),且延迟期间仍受唯一性约束
uniqueId() 和 uniqueFor() 怎么配才不翻车
uniqueId() 决定“哪些任务算同一个”,uniqueFor() 决定“这个‘同一个’管多久”。二者必须按业务语义对齐,否则要么去重失效,要么误杀合法任务。
使用场景:用户提交订单后触发发券任务,要确保同一用户 5 分钟内只发一次券。
- 写法正确:
public function uniqueId(): string { return 'coupon_' . $this->userId; }+public function uniqueFor(): int { return 300; } - 错误写法1:
uniqueId()返回$this->orderId—— 每次订单不同,永远不重复,去重失效 - 错误写法2:
uniqueFor()设为1秒 —— 网络抖动下两次请求毫秒级差,锁已过期,照样并发执行 - 性能影响:指纹越短、越固定(如纯数字 ID),Redis 查找越快;含 JSON 或长字符串会拖慢序列化
uniqueVia() 指定 Redis 连接时的硬限制
不能随便写 Redis::connection('xxx') 就完事。Laravel 的 ShouldBeUnique 要求该连接必须返回一个实现了 Illuminate\Contracts\Cache\Store 的实例,而原生 Redis::connection() 返回的是 Illuminate\Redis\Connections\Connection ——类型不匹配,直接报错 Call to undefined method store()。
正确做法只有两种:
- 用缓存配置项:在
config/cache.php里定义一个以redis为 driver 的缓存连接(如'unique_jobs' => ['driver' => 'redis', 'connection' => 'speed']),然后在 Job 中写public function uniqueVia() { return Cache::store('unique_jobs'); } - 或直接复用现有缓存连接名,如
return Cache::store('redis');(前提是cache.stores.redis配置有效) - 别传
Redis实例、别传Predis\Client、别用DB::connection('redis')—— 全都不认
和 withoutOverlapping() 不是一个东西,别混用
withoutOverlapping() 是给 Artisan 命令用的,锁的是「当前服务器上这个命令是否正在跑」;ShouldBeUnique 是给队列 Job 用的,锁的是「整个应用中这个任务实例是否已在队列或执行中」。两者作用域、存储介质、超时逻辑全不同。
容易踩的坑:
- 在调度里写
$schedule->job(new SendEmailJob())->withoutOverlapping()—— 语法错误,withoutOverlapping()只接受command()或闭包 - 以为给 Job 加了
ShouldBeUnique就不用withoutOverlapping()—— 错。如果这个 Job 是被command包裹调用的(比如php artisan job:dispatch),那命令层仍需防重叠 - 锁过期时间设得太短:例如
uniqueFor()设 60 秒,但任务平均耗时 80 秒,锁提前释放,第二个实例就进来了
最常被忽略的其实是锁过期时间与任务实际执行时间的对齐——它不是拍脑袋定的,得看监控里 queue.jobs.duration 的 P95 值,再往上加 20% 才安全。











