如何在我的Rails 4模型中修复和测试竞争条件

时间:2016-05-22 09:43:38

标签: ruby-on-rails ruby rspec

我的模型中有以下代码。有人正确地指出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)?

任何帮助非常感谢:)谢谢!

2 个答案:

答案 0 :(得分:2)

完全绕过问题的最简单方法是将默认条目的引用保留在别处,而不是依赖于布尔值true / false列。

因此,您可以拥有一个default_entities表,其中polymorphic association表示状态:

entity_id | entity_type
-----------------------
        1 |       State

现在更新默认值只需要修改一行,因此是atomic operation

即使您使用单独的键值存储,它也将是一个原子操作,但在这种情况下,您必须自己处理事务故障的回滚。

或者,如果由于某种原因您想保留现有架构,我建议您使用PostgreSQL advisory locks而不是锁定整个表。

引用此good introduction

  

[Postgres咨询锁]是应用程序强制数据库锁。   可以在会话级别和在会话级别获取咨询锁定   事务级别和会话结束时的预期释放或a   交易完成。

因此,您可以在更新默认状态之前尝试获取锁定,如果成功,则更新表格然后释放它。如果获取锁定失败,您可以重试固定的次数。

这里的优点是状态表上的其他不相关操作不会受到阻碍。

答案 1 :(得分:0)

出现竞态条件是因为您使用两个查询来设置default值。

仅使用一个查询怎么样:

def make_default!
  State.update_all(['default = (id = ?)', id])
end