Activerecord 中的悲观锁定 - with_lock 未按预期工作
Pessimistic locking in Activerecord - with_lock is not working as expected
我正在尝试为 User
模型实施 create_or_update 方法,这将创建新记录或更新现有记录。
def create_or_update_user(external_user_id, data)
user = User.find_or_initialize_by(external_user_id: external_user_id)
user.title = data[:title].downcase
# and so on - here we assign other attributes from data
user.save! if user.changed?
end
这里的陷阱是这个 table 正在由不同的进程同时更新,当他们试图用相同的 external_user_id
修改 user
时,竞争条件发生并且 ActiveRecord::RecordNotUnique
被提出。我尝试使用数据库锁来解决这个问题,但它没有按预期工作 - 有时仍然会引发异常。
table 结构如下所示:
create_table :users do |t|
t.integer :external_user_id, index: { unique: true }
# ...
end
更新方法 - 我在这里做错了什么?:
def create_or_update_user(external_user_id, data)
user = User.find_or_initialize_by(external_user_id: external_user_id)
user.with_lock do
user.title = data[:title].downcase
# and so on - here we assign other attributes from data
user.save! if user.changed?
end
end
我不能使用 upsert,因为我需要模型回调。
可能可以通过添加
来解决
rescue ActiveRecord::RecordNotUnique
retry
end
但我想使用锁以获得最佳性能,因为在这部分代码中竞争条件并不罕见。
UPD:添加了重现此竞争条件的要点
https://gist.github.com/krelly/520a2397f64269f96489c643a7346d0f
如果我的理解正确,在两个线程试图为不存在的外部 ID 创建新用户的情况下,该错误可能仍然会发生。
在这种情况下,线程 #1 将尝试在不存在的行上获取行锁 - 实际上不会发生锁定。
线程 #2 可以在线程 #1 仍在构建记录时进行相同的 find_or_initialize 查询,因此线程 #2 一旦提交就会遇到唯一性冲突。
正如您已经猜到的那样,解决此问题的唯一简单方法是救援 + 重试。线程 #2 会再试一次,它会更新线程 #1 创建的记录。
另一种解决方案是 advisory lock - 具有 application-specific 行为的锁。在这种情况下,您会说一次只有一个线程可以 运行 create_or_update_user
具有特定的外部 ID。
Rails 本身不支持咨询锁,但链接的文章包含一个示例,说明如何使用 Postgres 进行此操作。还有 gem with_advisory_locks
我正在尝试为 User
模型实施 create_or_update 方法,这将创建新记录或更新现有记录。
def create_or_update_user(external_user_id, data)
user = User.find_or_initialize_by(external_user_id: external_user_id)
user.title = data[:title].downcase
# and so on - here we assign other attributes from data
user.save! if user.changed?
end
这里的陷阱是这个 table 正在由不同的进程同时更新,当他们试图用相同的 external_user_id
修改 user
时,竞争条件发生并且 ActiveRecord::RecordNotUnique
被提出。我尝试使用数据库锁来解决这个问题,但它没有按预期工作 - 有时仍然会引发异常。
table 结构如下所示:
create_table :users do |t|
t.integer :external_user_id, index: { unique: true }
# ...
end
更新方法 - 我在这里做错了什么?:
def create_or_update_user(external_user_id, data)
user = User.find_or_initialize_by(external_user_id: external_user_id)
user.with_lock do
user.title = data[:title].downcase
# and so on - here we assign other attributes from data
user.save! if user.changed?
end
end
我不能使用 upsert,因为我需要模型回调。
可能可以通过添加
来解决rescue ActiveRecord::RecordNotUnique
retry
end
但我想使用锁以获得最佳性能,因为在这部分代码中竞争条件并不罕见。
UPD:添加了重现此竞争条件的要点 https://gist.github.com/krelly/520a2397f64269f96489c643a7346d0f
如果我的理解正确,在两个线程试图为不存在的外部 ID 创建新用户的情况下,该错误可能仍然会发生。
在这种情况下,线程 #1 将尝试在不存在的行上获取行锁 - 实际上不会发生锁定。
线程 #2 可以在线程 #1 仍在构建记录时进行相同的 find_or_initialize 查询,因此线程 #2 一旦提交就会遇到唯一性冲突。
正如您已经猜到的那样,解决此问题的唯一简单方法是救援 + 重试。线程 #2 会再试一次,它会更新线程 #1 创建的记录。
另一种解决方案是 advisory lock - 具有 application-specific 行为的锁。在这种情况下,您会说一次只有一个线程可以 运行 create_or_update_user
具有特定的外部 ID。
Rails 本身不支持咨询锁,但链接的文章包含一个示例,说明如何使用 Postgres 进行此操作。还有 gem with_advisory_locks