ActiveRecord模型的互斥锁

时间:2014-07-04 05:13:53

标签: 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

例如,这会打破一切:

user = User.first
Thread.new { user.nasty_long_running_method }
Thread.new { user.nasty_long_running_method }

但这没关系,应该允许:

user1 = User.find(1)
user2 = User.find(2)
Thread.new { user1.nasty_long_running_method }
Thread.new { user2.nasty_long_running_method }

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

5 个答案:

答案 0 :(得分:6)

我在搜索问题的解决方案时找到了一个gem Remote lock。它是一种在后端使用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

答案 1 :(得分:4)

我首先要添加互斥锁或信号量。阅读有关互斥锁: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

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

def nasty
  User.transaction do
    lock!
    ...
    save!
  end
end

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

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

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

数据库应该有一个唯一的索引,涵盖与项的多态关联的行。这应该保护多个线程不会同时锁定同一个项目。你的方法看起来像这样:

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

  ...

  semaphore.release
end

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

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

答案 2 :(得分:1)

我建议重新考虑您的体系结构,因为这不可扩展 - 假设有多个ruby进程,进程失败,超时等。同时进程锁定和生成线程对于应用程序服务器来说非常危险。

如果您希望熟悉生产,那么请尝试使用串行队列进行长时间运行任务的异步后台处理框架,这将确保运行任务的顺序。只需简单的RabbitMQ或查看此QA Best practice for Rails App to run a long task in the background?,最终尝试数据库但乐观锁定。

答案 3 :(得分:1)

您声明这是一个ActiveRecord模型,在这种情况下,通常的方法是在该记录上使用数据库锁。就我所见,无需额外的锁定机制。

看看关于悲观锁定的短(一页)Rails指南部分 - http://guides.rubyonrails.org/active_record_querying.html#pessimistic-locking

基本上你可以锁定单个记录或整个表(如果你正在更新很多东西)

在你的情况下,这样的事情应该可以解决问题......

class User < ActiveRecord::Base
  ...
  def nasty_long_running_method
    with_lock do
      // 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
end

答案 4 :(得分:1)

我最近创建了一个名为szymanskis_mutex的{​​{3}}。您可以在类User中包含该模块,并提供方法mutual_exclusion(concern)来提供所需的功能。

它不依赖数据库,也不依赖于在任何给定时刻要进入关键部分的进程数。

请注意,如果在不同的服务器中初始化了该类,则它将不起作用。

如果您的应用足够小,我可以满足您的需求。您的代码如下所示:

class User
  include SzymanskisMutex
  ...
  def nasty_long_running_method
    mutual_exclusion(:nasty_long) do
      http_request_1 // Takes 1-3 seconds.
      http_request_2 // Takes 1-3 seconds.
    end
    update_model
  end
end