我的模型中有以下代码。有人正确地指出make_default!
方法可能导致竞争条件:
class State < ActiveRecord::Base
def make_default!
State.update_all(default: false)
update!(default: true)
end
end
因为如果两个人同时更新记录,则最终可能会将两个State
个对象设置为default: true
。
我使用ActiveRecord锁来使用此解决方法:
class State < ActiveRecord::Base
def make_default!
# Prevent race condition using database-level locks.
State.transaction do
State.where.not(id: id).lock(true).update_all(default: false)
State.where(id: id).lock(true).first.update!(default: true)
end
end
end
但是有人指出这可能会导致其他潜在的错误。 我想知道实现锁定的最佳方法是什么,以及我如何测试模型规范(使用RSpec)?
任何帮助非常感谢:)谢谢!
答案 0 :(得分:2)
完全绕过问题的最简单方法是将默认条目的引用保留在别处,而不是依赖于布尔值true / false列。
因此,您可以拥有一个default_entities
表,其中polymorphic association表示状态:
entity_id | entity_type
-----------------------
1 | State
现在更新默认值只需要修改一行,因此是atomic operation。
即使您使用单独的键值存储,它也将是一个原子操作,但在这种情况下,您必须自己处理事务故障的回滚。
或者,如果由于某种原因您想保留现有架构,我建议您使用PostgreSQL advisory locks而不是锁定整个表。
[Postgres咨询锁]是应用程序强制数据库锁。 可以在会话级别和在会话级别获取咨询锁定 事务级别和会话结束时的预期释放或a 交易完成。
因此,您可以在更新默认状态之前尝试获取锁定,如果成功,则更新表格然后释放它。如果获取锁定失败,您可以重试固定的次数。
这里的优点是状态表上的其他不相关操作不会受到阻碍。
答案 1 :(得分:0)
出现竞态条件是因为您使用两个查询来设置default
值。
仅使用一个查询怎么样:
def make_default!
State.update_all(['default = (id = ?)', id])
end