登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

Laravel 队列任务为什么重复执行:唯一键、失败重试与幂等表的检查顺序

来源:17golang原创

时间:2026-08-26 21:35:24 336浏览 收藏

订单支付回调在一分钟内落了两条相同流水,队列监控里却只显示一次失败。Laravel 队列任务“重复执行”时,先不要急着把重试次数改成 0:真正需要确认的是任务是否被重复投递、是否在超时后被再次领取,以及业务写入有没有幂等边界。

要点速览:
  • 先用任务标识、业务单号和数据库写入时间确认是不是同一次任务。
  • 唯一键解决重复入队,幂等记录解决重复消费,失败重试解决暂时性故障,三者不能互相替代。
  • 调整超时、重试和数据库事务后,要用一条可重复的验收任务验证结果,再决定是否扩大范围。

先分清:重复投递、重复领取,还是业务重复写入

排查第一步只做证据归类。给任务 payload 增加业务单号和稳定的请求标识,查看应用日志、队列失败记录与目标表的写入时间。如果两条任务的 payload 标识不同,问题多半在生产端重复入队;如果标识相同但间隔接近 worker 超时,重点看任务是否在原 worker 仍未结束时被重新领取;如果队列里只有一条记录而目标表出现两行,才把数据库唯一约束和事务范围放到最前面。

我更建议先保留原始日志,不要先清理失败记录。没有这一步,后面很容易把“重试造成的第二次消费”误判成“业务方点了两次按钮”。

Laravel 队列重复任务的投递、领取和数据库写入信号对照图

先挡住重复入队:唯一键要覆盖业务动作

如果一个订单只能生成一张发票,唯一性应该落在订单号和动作类型上,而不是随机任务 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']);
});

外部支付、邮件或对象存储调用不要假装成数据库事务的一部分。需要把外部动作拆成可查询的状态,或者用供应方支持的幂等键;否则数据库回滚了,外部动作却已经完成,下一次重试仍可能再次发送。

Laravel 队列任务从唯一键到幂等记录的处理与回滚边界

失败重试怎么定:先区分暂时性故障

连接超时、短暂的上游 5xx、锁等待通常值得重试;参数校验失败、订单已关闭、权限不足则应尽快失败并进入人工处理。把所有异常都交给自动重试,会让重复副作用不断发生,也会让真正的坏数据被延后发现。

每次重试都要记录 attempt 次数、上次错误类型、租约到期时间和幂等键。重试退避应留出上游恢复时间,最长等待不能超过业务时限。修改 worker 的超时参数时,同时检查队列连接的可见性超时;前者太短会导致任务未完成就被再次领取,后者太长则会让真正的故障恢复变慢。

回滚和验收:用一条可重放任务收口

  1. 先暂停新业务入口或限制到一小组订单,保留当前队列和应用日志。
  2. 查询幂等表、目标业务表和失败任务记录,确认重复行是否已被唯一约束拦截。
  3. 用一个不会触发真实外部扣款的测试订单投递两次,检查队列任务、幂等记录和最终业务行。
  4. 让测试任务主动抛出一次可重试异常,再确认恢复后只完成一次业务写入。
  5. 观察一段完整的 worker 超时窗口,确认没有第二个 worker 同时处理同一租约。

如果改动后重复现象消失但失败任务数量异常上升,先回滚重试参数,再单独检查异常分类。不要把“队列不再重复”当成唯一成功标准,业务结果、日志证据和恢复路径必须一起成立。

常见问题

给任务设置唯一 ID 就能防止重复执行吗?

不能。唯一 ID 主要帮助识别任务,不能替代数据库唯一索引、幂等记录和外部动作的幂等键。

为什么失败任务重试后会出现两条业务记录?

通常是业务写入没有唯一约束,或写入成功后 worker 在确认完成前中断。重试再次进入时,如果没有状态检查,就会再次插入。

应该把重试次数直接改成 0 吗?

只有确认错误不可恢复且需要人工介入时才适合这样做。网络抖动、锁等待等暂时性错误仍应保留有限重试,并记录清晰的失败原因。

总结

Laravel 队列重复执行的处理顺序可以固定为:先辨认重复信号,再用业务唯一键挡住重复入队,用事务内幂等记录保护重复消费,最后按错误类型设置重试和回滚。每次变更都用可重放测试任务验收,才能知道修复的是投递问题,还是只把症状藏进失败队列。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>