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
相关产品推荐
相关产品推荐

