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

Rails 7+PostgreSQL应用中悲观锁策略失效问题

问题原因

你代码里的核心问题是锁的时机错误:每个线程先执行Stat.find(stat_id)拿到旧版本的data,再进入with_lock块执行更新。这会导致多个线程提前获取到相同的旧数据,即便后续加锁,更新时用的依然是之前拿到的旧数据合并结果,最终后面的线程会覆盖前面的修改,所以data的长度是随机的。

修复方案

要保证每次更新都基于数据库的最新数据,有两种可靠的解决方式:

方式一:将查询与更新放入同一锁块

把find操作移到锁内部,确保每次获取的都是当前数据库的最新记录:

threads = 20.times.map.with_index do |_, index|
  Thread.new do
    Stat.find(stat_id).with_lock do |stat|
      stat.update!(data: stat.data.merge({index => "ok"}))
    end
  end
end

threads.map &:join
stat = Stat.find(stat_id)
expect(stat.data.length).to eq(20)

with_lock内部会自动开启事务并对记录加行级锁(SELECT ... FOR UPDATE),确保同一时间只有一个线程能修改该记录。

方式二:用PostgreSQL原子更新jsonb(性能更优)

直接借助PostgreSQL的jsonb原生操作做原子合并,不需要加锁也能保证并发安全:

threads = 20.times.map.with_index do |_, index|
  Thread.new do
    Stat.where(id: stat_id).update_all(
      "data = data || '#{ActiveSupport::JSON.encode({index => "ok"})}'::jsonb"
    )
  end
end

threads.map &:join
stat = Stat.find(stat_id)
expect(stat.data.length).to eq(20)

注意:update_all会跳过Rails模型的回调和验证,如果需要保留这些逻辑,优先选择第一种加锁方式。

关键原理
  • 加锁方式:通过行级锁限制同一时间只有一个线程修改记录,每次更新都基于最新的数据库数据。
  • 原子更新方式:利用PostgreSQL的||操作符对jsonb字段做原子合并,数据库层面保证操作的不可分割性,性能比加锁更高效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 22:22:15