You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP多Worker并发执行时重复获取Postgres任务的原因排查

为什么多个PHP Worker会重复获取同一条Postgres任务?

这事儿核心问题出在数据库操作的原子性缺失上。你现在的代码是两步走:先执行Job::first()查询出第一条任务,然后再调用delete()删除它。在并发场景下,多个Worker会同时走到first()这一步——因为Postgres在执行普通SELECT的时候不会加锁,所以多个Worker会同时拿到同一条任务记录,接着各自尝试删除,这时候就会出现重复执行的情况了。

举个直观的例子:Worker A和Worker B几乎同时执行Job::first(),都拿到了ID=1的任务,然后A先执行delete()成功,B紧接着执行delete()(这时候这条记录其实已经没了,但你的代码里判断$job->delete()==true可能因为ORM的缓存或者判断逻辑问题,误以为删除成功,进而执行后续的任务处理逻辑)。


怎么解决这个问题?

核心思路是把**“获取任务”和“锁定/删除任务”变成一个原子操作**,让数据库保证同一时间只有一个Worker能拿到某条任务。Postgres正好提供了完美的解决方案:SELECT ... FOR UPDATE SKIP LOCKED。

方案1:用Postgres的行级锁+跳过已锁定记录(推荐)

这个方法会让Worker查询第一条未被锁定的任务,同时立刻给这条记录加排他锁,其他Worker再查询时会自动跳过被锁定的记录,从下一条未被锁定的任务开始取。

如果用Laravel的话,可以这么写:

DB::beginTransaction();
try {
    // 查询并锁定第一条未被锁定的任务,跳过已被其他Worker锁定的
    $job = Job::lockForUpdate()->skipLocked()->first();
    
    if ($job) {
        // 这里可以先标记任务为"处理中",或者直接删除
        $job->delete();
        DB::commit();
        
        // 执行任务处理逻辑
        // do something
    } else {
        DB::commit();
        // 没有任务可处理,直接退出或者等待
    }
} catch (\Exception $e) {
    DB::rollBack();
    // 处理异常
}

方案2:用原生SQL实现原子删除

另一种思路是直接在一个SQL语句里完成“查询+删除”,利用Postgres的RETURNING子句,把删除的记录返回回来,这样整个操作是原子的,不会出现并发冲突:

$job = DB::selectOne("DELETE FROM jobs WHERE id = (SELECT id FROM jobs ORDER BY id LIMIT 1) RETURNING *");

if ($job) {
    // 执行任务处理逻辑
    // do something
}

这个方法的好处是不需要手动管理事务,因为单个SQL语句本身就是原子的。


额外注意事项
  • 不要依赖ORM的缓存:比如有些ORM会把查询到的对象缓存起来,即使数据库里记录已经被删除,对象的属性还在,导致后续判断出错。
  • 优先标记任务状态而非直接删除:如果任务处理过程中Worker崩溃,直接删除会导致任务丢失。更好的做法是先把任务标记为processing,处理完成后再删除或者标记为completed,这样即使Worker挂了,后续可以重新处理状态为processing且超时的任务。

内容的提问来源于stack exchange,提问作者Gediminas Šukys

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 08:18:32