如何防止并行Sidekiq作业在Rails中执行代码

时间:2015-02-12 16:34:25

标签: ruby-on-rails multithreading sidekiq rails-activejob

我有大约10名工作人员执行包括以下内容的工作:

user = User.find_or_initialize_by(email: 'some-email@address.com')

if user.new_record?
# ... some code here that does something taking around 5 seconds or so
elsif user.persisted?
# ... some code here that does something taking around 5 seconds or so
end

user.save

问题是,在某些时候,两个或更多工作人员在确切的时间运行此代码,因此我后来发现两个或更多用户具有相同的email,我应该总是在其中结束只有独特的电子邮件。

我的情况不可能为email创建数据库唯一索引,因为唯一的电子邮件是有条件的 - 有些用户应该有唯一的电子邮件,有些则没有。

值得一提的是,我的User模型具有唯一性验证,但它仍然无法帮助我,因为在.find_or_initialize_by.save之间,有一个依赖的代码如果已经创建了用户对象。

我尝试了悲观和乐观的锁定,但它没有帮助我,或者我只是没有正确实现它......如果你对此有一些建议。

我只能想到的解决方案是在执行这些代码行时锁定其他线程(Sidekiq作业),但我不太清楚如何实现这一点,也不知道这是否是一种可行的方法。

我将不胜感激。

修改

在我的具体案例中,将电子邮件参数放在工作中会很困难,因为这项工作比上面说的要复杂一些。该作业实际上是一个导出脚本,其中作业的一部分是上面的代码。我认为也不可能将上面的功能分成另一个单独的工作者......因为整个作业流程应该是串行的,并且不应该并行/异步地处理任何部分。此作业只是由另一个作业管理的作业之一,最终由主作业管理。

3 个答案:

答案 0 :(得分:2)

悲观锁定是您想要的,但只适用于存在的记录 - 您不能将其与new_record?一起使用,因为尚未锁定数据库。

答案 1 :(得分:1)

我建议采用不同的架构来绕过这个问题。

生产者 - 工人模型怎么样,其中一个主Sidekiq进程获取电子邮件地址列表,然后为每个电子邮件产生一个工人Sidekiq进程? Sidekiq通过专用队列让主人和工人进行交流,从而轻松实现这一目标。

这样做,电子邮件地址成为工人的输入参数,因此我们知道 by construction 工作人员不会相互依赖数据。

答案 2 :(得分:0)

我设法用以下方法解决了我的问题:

我发现我实际上可以在Rails DB Uniqueness Partial Index中添加where子句,因此我现在可以在数据库级别为不同类型的用户设置唯一性条件,其中其他并发作业将现在如果已经创建了ActiveRecord::RecordNotUnique错误。

现在唯一的问题是.find_or_initialize_by.save之间的代码,因为这些代码与User对象有时间关系,其中只有一个并发作业总是应该得到{{1}然后,其他并发作业应该触发.new_record? == true,因为一个作业永远是第一个创建它,但是......所有这些作业还没有工作,因为它只在行{{1调用db唯一性索引验证的位置。因此,我设法通过在这些条件之前放置.persisted? == true来解决这个问题,同时我为.save添加了一个救援块,然后在触发{.save时将另一个作业添加到自己的队列中{1}}错误,以确保异步作业不会发生冲突。代码现在如下所示。

.save