ActiveRecord模型的互斥锁

Mik*_*ika 8 ruby ruby-on-rails-4

我的用户模型有一个讨厌的方法,不应该同时为同一记录的两个实例调用.我需要连续执行两个http请求,同时确保任何其他线程不会同时为同一个记录执行相同的方法.

class User
  ...
  def nasty_long_running_method
    // something nasty will happen if this method is called simultaneously
    // for two instances of the same record and the later one finishes http_request_1
    // before the first one finishes http_request_2.
    http_request_1 // Takes 1-3 seconds.
    http_request_2 // Takes 1-3 seconds.
    update_model
  end
end
Run Code Online (Sandbox Code Playgroud)

例如,这会破坏一切:

user = User.first
Thread.new { user.nasty_long_running_method }
Thread.new { user.nasty_long_running_method }
Run Code Online (Sandbox Code Playgroud)

但这没关系,应该允许:

user1 = User.find(1)
user2 = User.find(2)
Thread.new { user1.nasty_long_running_method }
Thread.new { user2.nasty_long_running_method }
Run Code Online (Sandbox Code Playgroud)

对于同一记录的两个实例,确保不同时调用该方法的最佳方法是什么?

Mik*_*ika 6

我在搜索问题的解决方案时找到了一个gem 远程锁.它是一种在后端使用Redis的互斥解决方案.

它:

  • 适用于所有流程
  • 不会锁定数据库
  • 在内存中 - >快速而无IO

该方法现在看起来像这样

def nasty
  $lock = RemoteLock.new(RemoteLock::Adapters::Redis.new(REDIS))
  $lock.synchronize("capi_lock_#{user_id}") do
    http_request_1
    http_request_2
    update_user
  end
end
Run Code Online (Sandbox Code Playgroud)


spi*_*ann 5

我会从添加互斥锁或信号量开始。阅读关于互斥锁:http : //www.ruby-doc.org/core-2.1.2/Mutex.html

class User

  ...
  def nasty
    @semaphore ||= Mutex.new

    @semaphore.synchronize {
      # only one thread at a time can enter this block...  
    }
  end
end
Run Code Online (Sandbox Code Playgroud)

如果您的类是一个ActiveRecord对象,您可能希望使用 Rails 的锁定和数据库事务。请参阅:http : //api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html

def nasty
  User.transaction do
    lock!
    ...
    save!
  end
end
Run Code Online (Sandbox Code Playgroud)

更新:您用更多详细信息更新了您的问题。似乎我的解决方案不再适合了。如果您有多个实例在运行,第一个解决方案将不起作用。第二个只锁定数据库行,不会阻止多个线程同时进入代码块。

因此,如果会考虑构建基于数据库的信号量。

class Semaphore < ActiveRecord::Base
  belongs_to :item, :polymorphic => true

  def self.get_lock(item, identifier)
    # may raise invalid key exception from unique key contraints in db
    create(:item => item) rescue false
  end

  def release
    destroy
  end
end
Run Code Online (Sandbox Code Playgroud)

数据库应该有一个唯一的索引来覆盖与项目的多态关联的行。这应该可以防止多个线程同时获得同一个项目的锁。您的方法如下所示:

def nasty
  until semaphore
    semaphore = Semaphore.get_lock(user)
  end

  ...

  semaphore.release
end
Run Code Online (Sandbox Code Playgroud)

有几个问题需要解决:你想等待多长时间来获得信号量?如果外部 http 请求需要很长时间,会发生什么?您是否需要存储额外的信息(主机名、pid)来识别哪个线程锁定了一个项目?您将需要某种清理任务来删除一段时间后或重新启动服务器后仍然存在的锁。

此外,我认为在 Web 服务器中拥有这样的东西是一个糟糕的主意。至少你应该将所有这些东西转移到后台工作中。如果您的应用程序很小并且只需要一项后台工作即可完成所有工作,那么什么可能会解决您的问题。