与Sidekiq Ruby on Rails背景工作的幂等设计

时间:2017-08-29 14:39:12

标签: ruby-on-rails ruby multithreading sidekiq

Sidekiq建议所有工作都是幂等的(能够多次运行而不会出现问题),因为它不能保证作业只能运行一次。

我无法理解在某些情况下实现这一目标的最佳方法。例如,假设您有下表:

用户 ID 电子邮件 平衡

运行的后台作业只会为其余额增加一些金额

def perform(user_id, balance_adjustment)
 user = User.find(user_id)
 user.balance += balance_adjustment
 user.save
end

如果此作业多次运行,其余额将不正确。对于这样的事情,最佳做法是什么?

如果我认为这是一个潜在的解决方案,我可以提出的是在安排工作之前创建一个记录,如

PendingBalanceAdjustment 用户身份 balance_adjustment

当作业运行时,它需要获得该用户的锁定,以便两个工人之间不存在竞争条件,然后需要更新余额并从待处理的余额调整中删除记录在释放锁之前。

那么这份工作看起来像这样?

def perform(user_id, balance_adjustment_id)
  user = User.find(user_id)
  pba = PendingBalanceAdjustment.where(:balance_adjustment_id => balance_adjustment_id).take
  if pba.present?
    $redis.lock("#{user_id}/balance_adjustment") do
      user.balance += pba.balance_adjustment
      user.save
      pba.delete    
    end
  end
end

这似乎解决了两个问题

a)同时接受工作的两名工人之间的竞争状况(虽然你认为Sidekiq可以保证这一点吗?) b)成功运行后多次运行的作业

这种模式是一个很好的解决方案吗?

2 个答案:

答案 0 :(得分:2)

你走在正确的轨道上;你想使用数据库事务,而不是redis锁。

答案 1 :(得分:0)

我认为你也走在了正确的轨道上,但是因为我对你的应用程序没有完全的了解,你的解决方案可能过度杀伤。

但是,一个更简单的解决方案就是在User模型上设置一个标记balance_updated:datetime。所以,你可以在更新之前检查一下。

正如迈克提到使用Transaction块应该确保它的线程安全。

在任何情况下,更一般地回答你的问题......拥有一个updated_列通常足以开始,然后如果它变得复杂,你可以将这些东西移动到另一个模型。