Laravel 队列任务为什么重复执行:唯一键、失败重试与幂等表的检查顺序
来源:17golang原创
时间:2026-08-26 21:35:24 336浏览 收藏
订单支付回调在一分钟内落了两条相同流水,队列监控里却只显示一次失败。Laravel 队列任务“重复执行”时,先不要急着把重试次数改成 0:真正需要确认的是任务是否被重复投递、是否在超时后被再次领取,以及业务写入有没有幂等边界。
- 先用任务标识、业务单号和数据库写入时间确认是不是同一次任务。
- 唯一键解决重复入队,幂等记录解决重复消费,失败重试解决暂时性故障,三者不能互相替代。
- 调整超时、重试和数据库事务后,要用一条可重复的验收任务验证结果,再决定是否扩大范围。
先分清:重复投递、重复领取,还是业务重复写入
排查第一步只做证据归类。给任务 payload 增加业务单号和稳定的请求标识,查看应用日志、队列失败记录与目标表的写入时间。如果两条任务的 payload 标识不同,问题多半在生产端重复入队;如果标识相同但间隔接近 worker 超时,重点看任务是否在原 worker 仍未结束时被重新领取;如果队列里只有一条记录而目标表出现两行,才把数据库唯一约束和事务范围放到最前面。
我更建议先保留原始日志,不要先清理失败记录。没有这一步,后面很容易把“重试造成的第二次消费”误判成“业务方点了两次按钮”。

先挡住重复入队:唯一键要覆盖业务动作
如果一个订单只能生成一张发票,唯一性应该落在订单号和动作类型上,而不是随机任务 ID。入队前可以先检查业务状态,数据库层仍要保留唯一索引,因为并发请求可能同时通过应用层判断。
$key = $orderId . ':invoice';
if (InvoiceJobRecord::where('idempotency_key', $key)->exists()) {
return;
}
InvoiceJobRecord::create([
'idempotency_key' => $key,
'order_id' => $orderId,
'state' => 'queued',
]);
GenerateInvoice::dispatch($orderId, $key);
这个片段还不能单独承诺幂等,因为“查询后插入”存在竞态。实际表上应为 idempotency_key 建唯一索引,并把插入冲突当成“已有任务”处理。唯一索引命中后,记录数据库异常、任务标识和业务单号,确认它确实是同一动作,不要无条件吞掉所有异常。
再处理重复消费:把幂等记录放进事务边界
队列 worker 可能在外部接口已经成功后才超时,也可能在数据库提交前崩溃。两种情况都要求任务代码能够再次进入。常见做法是用数据库事务锁定幂等记录:状态为 done 时直接返回,状态为 running 时根据租约时间判断是否允许接管,状态为 failed 时只对可重试错误重新进入。
DB::transaction(function () use ($key, $orderId) {
$record = InvoiceJobRecord::where('idempotency_key', $key)
->lockForUpdate()
->firstOrFail();
if ($record->state === 'done') {
return;
}
$record->update(['state' => 'running']);
Invoice::firstOrCreate(['order_id' => $orderId]);
$record->update(['state' => 'done']);
});
外部支付、邮件或对象存储调用不要假装成数据库事务的一部分。需要把外部动作拆成可查询的状态,或者用供应方支持的幂等键;否则数据库回滚了,外部动作却已经完成,下一次重试仍可能再次发送。

失败重试怎么定:先区分暂时性故障
连接超时、短暂的上游 5xx、锁等待通常值得重试;参数校验失败、订单已关闭、权限不足则应尽快失败并进入人工处理。把所有异常都交给自动重试,会让重复副作用不断发生,也会让真正的坏数据被延后发现。
每次重试都要记录 attempt 次数、上次错误类型、租约到期时间和幂等键。重试退避应留出上游恢复时间,最长等待不能超过业务时限。修改 worker 的超时参数时,同时检查队列连接的可见性超时;前者太短会导致任务未完成就被再次领取,后者太长则会让真正的故障恢复变慢。
回滚和验收:用一条可重放任务收口
- 先暂停新业务入口或限制到一小组订单,保留当前队列和应用日志。
- 查询幂等表、目标业务表和失败任务记录,确认重复行是否已被唯一约束拦截。
- 用一个不会触发真实外部扣款的测试订单投递两次,检查队列任务、幂等记录和最终业务行。
- 让测试任务主动抛出一次可重试异常,再确认恢复后只完成一次业务写入。
- 观察一段完整的 worker 超时窗口,确认没有第二个 worker 同时处理同一租约。
如果改动后重复现象消失但失败任务数量异常上升,先回滚重试参数,再单独检查异常分类。不要把“队列不再重复”当成唯一成功标准,业务结果、日志证据和恢复路径必须一起成立。
常见问题
给任务设置唯一 ID 就能防止重复执行吗?
不能。唯一 ID 主要帮助识别任务,不能替代数据库唯一索引、幂等记录和外部动作的幂等键。
为什么失败任务重试后会出现两条业务记录?
通常是业务写入没有唯一约束,或写入成功后 worker 在确认完成前中断。重试再次进入时,如果没有状态检查,就会再次插入。
应该把重试次数直接改成 0 吗?
只有确认错误不可恢复且需要人工介入时才适合这样做。网络抖动、锁等待等暂时性错误仍应保留有限重试,并记录清晰的失败原因。
总结
Laravel 队列重复执行的处理顺序可以固定为:先辨认重复信号,再用业务唯一键挡住重复入队,用事务内幂等记录保护重复消费,最后按错误类型设置重试和回滚。每次变更都用可重放测试任务验收,才能知道修复的是投递问题,还是只把症状藏进失败队列。
-
321 收藏
-
426 收藏
-
468 收藏
-
311 收藏
-
371 收藏
-
376 收藏
-
411 收藏
-
163 收藏
-
文章 · php教程 | 4小时前 | reflection · php教程 · PHP属性 · 运行时元数据 · php ReflectionAttribute getAttributes newInstance IS_REPEATABLE PHP属性235 收藏
-
270 收藏
-
128 收藏
-
文章 · php教程 | 10小时前 | 反射 · PHP · PHP 8.4 · ReflectionProperty · 属性钩子 · 反射 属性钩子 PHP 8.4 ReflectionProperty::isVirtual 虚拟属性438 收藏
-
372 收藏
-
134 收藏
-
文章 · php教程 | 19小时前 | PHP · 并发控制 · 线上排查 · 会话管理 · 配置验收 · php session.save_path PHP会话 session_write_close session.lazy_write469 收藏
-
346 收藏
-
文章 · php教程 | 1天前 | 反射 · attribute · php教程 · PHP 8 · 代码验收 · php ReflectionClass Attribute ReflectionAttribute getAttributes isRepeated134 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习