3

我有大约 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 作业),但我不太确定如何实现这一点,也不知道这是否是一种可建议的方法。

我将不胜感激任何帮助。

编辑

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

4

3 回答 3

2

悲观锁定是您想要的,但仅适用于存在的记录 - 您不能使用它,new_record?因为数据库中还没有任何东西可以锁定。

于 2015-02-12T16:46:04.303 回答
1

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

生产者-工作者模型怎么样,其中一个主 Sidekiq 进程获取电子邮件地址列表,然后为每封电子邮件生成一个工作者 Sidekiq 进程?Sidekiq 通过为 master 和 worker 通信的专用队列使这变得容易。

这样做,电子邮件地址成为工人的输入参数,因此我们通过构造知道工人不会在彼此的数据上进行交流。

于 2015-02-16T13:17:51.980 回答
0

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

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

现在唯一的问题是 and 之间的代码.find_or_initialize_by.save因为这些代码取决于用户对象,其中始终只有一个并发作业应该总是得到 a .new_record? == true,然后其他并发作业应该触发,.persisted? == true因为一个作业总是第一个创建它,但是......所有这些都不起作用,因为它只是在.save调用 db 唯一性索引验证的那一行。因此,我设法通过将.save这些条件放在前面来解决这个问题,同时我添加了一个救援块.save,然后在它触发ActiveRecord::RecordNotUnique错误时将另一个作业添加到自身的队列中,以确保异步作业不会得到冲突。代码现在如下所示。

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

begin
  user.save
  is_new_record = user.new_record?
  is_persisted = user.persisted?

rescue ActiveRecord::RecordNotUnique => exception
  MyJob.perform_later(params_hash)
end

if is_new_record
  # do something if not yet created
elsif is_persisted
  # do something if already created
end
于 2015-02-16T13:10:34.820 回答