轨道,理论和生产中的find_or_create和竞争条件

时间:2011-02-08 22:39:48

标签: ruby-on-rails ruby activerecord race-condition

嗨,我有这段代码

class Place < ActiveRecord::Base
  def self.find_or_create_by_latlon(lat, lon)
    place_id = call_external_webapi
    result = Place.where(:place_id => place_id).limit(1)
    result = Place.create(:place_id => place_id, ... ) if result.empty? #!
    result
  end
end

然后我想在另一个模型或控制器中做

p = Post.new
p.place = Place.find_or_create_by_latlon(XXXXX, YYYYY) # race-condition
p.save

Place.find_or_create_by_latlon 如果执行的操作是 create ,则有时需要太多时间来获取数据,有时在生产中 p.place 是为零。

在执行 p.save 之前,如何强制等待响应? 谢谢你的建议

3 个答案:

答案 0 :(得分:1)

你是对的,这是一种竞争条件,它通常可以由双击表单上的提交按钮的人触发。如果遇到错误,您可能会做的是回送。

result = Place.find_by_place_id(...) ||
  Place.create(...) ||
  Place.find_by_place_id(...)

有更优雅的方法可以做到这一点,但基本方法就在这里。

答案 1 :(得分:0)

我不得不处理类似的问题。在我们的后端,如果用户不存在,则从令牌创建用户。在创建用户记录之后,会发送一个缓慢的API调用来更新用户信息。

def self.find_or_create_by_facebook_id(facebook_id)
  User.find_by_facebook_id(facebook_id) || User.create(facebook_id: facebook_id)
rescue ActiveRecord::RecordNotUnique => e
  User.find_by_facebook_id(facebook_id)
end

def self.find_by_token(token)
  facebook_id = get_facebook_id_from_token(token)

  user = User.find_or_create_by_facebook_id(facebook_id)

  if user.unregistered?
    user.update_profile_from_facebook
    user.mark_as_registered
    user.save
  end

  return user
end

该策略的步骤是首先从create方法中删除缓慢的API调用(在我的例子中为update_profile_from_facebook)。由于操作需要很长时间,因此当您将操作作为创建调用的一部分包含在内时,显着增加了重复插入操作的可能性。

第二步是向数据库列添加唯一约束,以确保不会创建重复项。

最后一步是创建一个函数,在少数情况下捕获RecordNotUnique异常,其中重复的插入操作被发送到数据库。

这可能不是最优雅的解决方案,但它对我们有用。

答案 2 :(得分:0)

我在一个sidekick工作中点击这个,重复并重复获取错误并最终清除。我发现的最好的解释是在博客here上。要点是postgres保留了一个内部存储的值,用于递增以某种方式搞砸的主键。这对我来说是正确的,因为我正在设置主键,而不仅仅是使用递增的值,因此可能会出现这种情况。上面链接中的评论解决方案似乎是致电ActiveRecord::Base.connection.reset_pk_sequence!(table_name)这为我解决了这个问题。

begin
   result = Place.where(:place_id => place_id).limit(1)
   result = Place.create(:place_id => place_id, ... ) if result.empty? #!
rescue ActiveRecord::StatementInvalid => error
   @save_retry_count =  (@save_retry_count || 1)
   ActiveRecord::Base.connection.reset_pk_sequence!(:place)
   retry if( (@save_retry_count -= 1) >= 0 )
   raise error
end