我有大约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作业),但我不太清楚如何实现这一点,也不知道这是否是一种可行的方法。
我将不胜感激。
修改
在我的具体案例中,将电子邮件参数放在工作中会很困难,因为这项工作比上面说的要复杂一些。该作业实际上是一个导出脚本,其中作业的一部分是上面的代码。我认为也不可能将上面的功能分成另一个单独的工作者......因为整个作业流程应该是串行的,并且不应该并行/异步地处理任何部分。此作业只是由另一个作业管理的作业之一,最终由主作业管理。
答案 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