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

ActiveRecord操作PostgreSQL遇竞态条件:锁机制失效求助

解决PostgreSQL并发任务重复执行的锁机制问题

这个并发抢占任务的坑我之前踩过,核心问题就是你现在的代码把「查询未启动任务」和「更新状态」分成了两步操作,中间有时间间隙,多个脚本很容易在这间隙里拿到同一个任务。下面给你几个针对性的解决方案,从推荐到备选排序:

1. 行锁+事务(阻塞式,保证任务不丢失)

这是最稳妥的方案,利用PostgreSQL的行级排他锁,在事务内原子完成查询和更新,确保同一时间只有一个脚本能拿到目标任务:

def find_unstarted_job
  Jobs.transaction do
    # 找到第一个未启动任务并立即加行锁,其他事务会阻塞到当前事务提交
    job = Jobs.where(status: :unstarted).lock('FOR UPDATE').first
    if job
      job.started!
      job
    end
  end
end

为什么这能生效?

  • transaction包裹确保所有操作要么一起成功要么一起回滚,避免中间状态暴露给其他脚本
  • lock('FOR UPDATE')会给查询到的行加上排他锁,其他事务要读取或修改这行时会被阻塞,直到当前事务提交(也就是started!执行完成后),从根本上杜绝了竞态条件

2. 跳过锁定行(非阻塞高并发场景)

如果你的任务量很大,不想让脚本因为等待锁而阻塞,可以用FOR UPDATE SKIP LOCKED,直接跳过已经被其他事务锁定的任务,拿下一个可用的:

def find_unstarted_job
  Jobs.transaction do
    job = Jobs.where(status: :unstarted).lock('FOR UPDATE SKIP LOCKED').first
    if job
      job.started!
      job
    end
  end
end

适用场景:

  • 任务数量足够多,允许跳过暂时被锁定的任务
  • 追求高并发吞吐量,不想让脚本长时间等待锁

3. 表锁方案(不推荐但满足你的需求)

如果你确实需要锁定整张表(不推荐,会严重影响并发性能),可以用UPDATE ... RETURNING *原子完成更新和查询,同时在事务内锁表:

def find_unstarted_job
  Jobs.transaction do
    # 先给整张表加排他锁(注意:这会阻塞所有其他对jobs表的操作)
    ActiveRecord::Base.connection.execute("LOCK TABLE jobs IN ACCESS EXCLUSIVE MODE;")
    # 原子更新并返回结果,避免两次操作的间隙
    job = Jobs.find_by_sql([
      "UPDATE jobs SET status = $1 WHERE status = $2 LIMIT 1 RETURNING *;",
      Jobs.statuses[:started],
      Jobs.statuses[:unstarted]
    ]).first
    job
  end
end

注意事项:

  • ACCESS EXCLUSIVE是PostgreSQL最严格的表锁,会阻塞所有读、写操作,只有当前事务能操作表,除非业务必须,否则别用
  • UPDATE ... RETURNING *是原子操作,直接把查询和更新合并成一步,完美解决你之前用表锁时无法返回结果的问题

为什么你之前的尝试没生效?

你提到的.lock、.transaction没起作用,大概率是因为:

  • 没有把查询和更新逻辑放在同一个事务块里,导致锁的作用范围失效
  • 单纯用.lock(true)而没有指定FOR UPDATE,默认的锁级别不足以阻止并发读写
  • 先查询再更新的两步操作之间存在间隙,给了其他脚本抢占的机会

内容的提问来源于stack exchange,提问作者Joe

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:34:34