我的用户模型有一个讨厌的方法,不应该同时为同一记录的两个实例调用。我需要连续执行两个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 }
确保同一记录的两个实例不同时调用该方法的最佳方法是什么?
答案 0 :(得分:6)
我在搜索问题的解决方案时找到了一个gem Remote lock。它是一种在后端使用Redis的互斥解决方案。
有:
该方法现在看起来像这样
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