【问题标题】:Idempotent Design with Sidekiq Ruby on Rails Background JobSidekiq Ruby on Rails 后台作业的幂等设计
【发布时间】:2018-02-07 01:55:06
【问题描述】:

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

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

用户 ID 电子邮件 余额

运行的后台作业只是增加了一些余额

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

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

如果我考虑一下,我可以想出的一个潜在解决方案是在安排类似的工作之前创建一个记录

PendingBalanceAdjustment 用户身份 平衡调整

当作业运行时,它需要为该用户获取一个锁,这样两个工作人员之间就不会出现竞争条件,然后在释放锁之前需要更新余额并从待处理的余额调整中删除记录.

然后工作看起来像这样?

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) 一个作业运行成功后被多次运行

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

【问题讨论】:

    标签: ruby-on-rails ruby multithreading sidekiq


    【解决方案1】:

    我认为您也走在正确的轨道上,但您的解决方案可能过于矫枉过正,因为我对您的应用程序并不了解。

    但是,一个更简单的解决方案就是在您的 User 模型上添加一个标志,例如 balance_updated:datetime。所以,你可以在更新之前检查一下。

    正如 Mike 提到的,使用 Transaction 块应该确保它是线程安全的。

    无论如何,为了更笼统地回答您的问题...从一个 updated_ 列开始通常就足够了,然后如果变得复杂,您可以将这些东西移到另一个模型中。

    【讨论】:

    • 感谢您的想法。我认为这个解决方案的问题是现在你必须引入一个任意时间限制来允许多久更新一次列?是这个主意吗?因此,该工作将检查余额是否在从现在起的某个小时间增量内更新,如果低于该阈值则保释?
    • 另外,使用这个解决方案,您仍然需要一个锁,因为两个线程仍然可以在时间戳更新之前读取该行并且两个作业仍然会运行
    【解决方案2】:

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

    【讨论】:

    • 嘿!感谢您的即时回复。我可以将余额调整和挂起的行删除放在一个数据库事务中,但这不一定能防止两个工作人员同时尝试运行事务的多线程并发问题?
    • 对于与此类似的每项工作都必须拥有某种 PendingABC 记录,这似乎也有点矫枉过正/大量开销。这真的是最好的模式吗?
    • 通常有一个贷记/借记表来跟踪每次货币调整的历史记录。 “user.balance”列只是成为 sum(PBA.amount) 的缓存副本。我会使用 SQL 而不是 Ruby 原子地做这些事情。单个 UPDATE 语句比 Ruby 中的多个网络调用更快/更可靠。
    猜你喜欢
    • 1970-01-01
    • 2013-04-08
    • 2012-11-06
    • 1970-01-01
    • 2016-06-17
    • 1970-01-01
    • 2021-01-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多