【问题标题】:Ruby: How to handle optimistic locking using update_all(attributes)Ruby:如何使用 update_all(attributes) 处理乐观锁定
【发布时间】:2018-10-03 10:18:15
【问题描述】:

我正在尝试实现竞争条件的乐观锁定。为此,我在 Product: Model through migration 中添加了一个额外的列 lock_version

#Product: Model's new field:
    #  attribute_1
    #  lock_version                       :integer(4)      default(0), not null
before_validation :method_1, :if => :recalculation_required_attribute

def method_1
    ####
    ####
    if self.lock_version == Product.find(self.id).lock_version
       Product.where(:id => self.id).update_all(attributes)
       self.attributes = attributes
       self.save!
    end
end

产品型号有一个attribute_1。如果attribute_1 需要重新计算,则before_validation: method_1 将调用。

我正在使用 lock_version 进行乐观锁定。但是,update_all 不会增加lock_version。所以我开始使用save!。现在我收到一个新错误:SystemStackError: stack level too deep,因为self.save! 触发了before_validation: method1。在上述情况下如何停止无限循环的回调并处理乐观锁定。

【问题讨论】:

  • 那么您要更新哪个对象的lock_versionself 或所有Product.where(:id => self.id)。你的代码太复杂了......你为什么在Product模型中做Product.find(self.id)id 不是唯一的主键吗?

标签: ruby-on-rails ruby ruby-on-rails-3 ruby-on-rails-4 race-condition


【解决方案1】:

可能的解决方案:

class Product < ApplicationRecord    
  before_validation :reload_and_apply_changes_if_stale, on: :update

  def reload_and_assign_changes_if_stale
    # if stale
    if lock_version != Post.find(id).lock_version
      # store the "changes" first into a backup variable
      current_changes = changes

      # reload this record from "real" up-to-date values from DB (because we already know that it's stale)
      reload
      # after reloading, `changes` now becomes `{}`, and is why we need the backup variable `current_changes` above

      # now finally, assign back again all the "changed" values
      current_changes.each do |attribute_name, change|
        change_from = change[0] # you can remove this line
        change_to = change[1]
        self[attribute_name] = change_to
      end
    end
  end
end

重要提示:

  • 上面的before_validation 仍然不能保证会避免竞争条件!因为看下面的例子:

    class Product < ApplicationRecord
      # this triggers first...
      before_validation :reload_and_apply_changes_if_stale, on: :update
      # then, this triggers next...
      before_update :do_some_heavy_loooong_calculation
    
      def do_some_heavy_loooong_calculation
        sleep(60.seconds)
        # ... of which during this time, this record might already be stale! as perhaps another "process" or another "server" has already updated this record!
      end
    
  • 确保上面的 before_validation 位于 Post 模型的最顶部,以便在任何其他 before_validations(甚至任何后续回调:*_update,或*_save),因为您可能有一个或两个后续回调,这些回调取决于属性的当前状态(即,它正在执行一些计算,或检查某个布尔标志属性),然后您需要先重新加载(如上所述),在进行这些计算之前。

  • 上面的before_validation 仅适用于模型回调中的“计算/依赖项”,但如果您在Product 模型的回调之外有计算/依赖项,则无法正常工作;即如果你有类似的东西:

    class ProductsController < ApplicationController
      def update
        @product = Product.find(params[:id])
    
        # let's assume at this line, @product.cost = nil (no value yet)
    
        @product.assign_attributes(product_attributes)
    
        # let's assume at this line, @product.cost = 1100
    
        # because 1100 > 1000, then DO SOME IMPORTANT THING!
        if @product.cost_was.nil? && @product.cost > 1_000.0
          # do some important thing!
        end
    
        # however, when `product.save` is called below and the `before_validation :reload_and_apply_changes_if_stale` is triggered,
        # of which let's say some other "process" has already updated this
        # exact same record, and thus @product is reloaded, but the real DB value is now
        # @product.cost = 900; there's no WAY TO UNDO SOME IMPORTANT THING! above
    
        @product.save
      end
    end
    

上面的注释是为什么默认情况下 Rails 不会自动重新加载该属性作为 before_validation 或其他东西,因为根据您的应用程序/业务逻辑,您可能想要“重新加载”或“不重新加载” ",这就是为什么默认情况下 Rails 会引发 ActiveRecord::StaleObjectError (see docs) 来专门救援,并在发生这种竞争条件时处理相应的操作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多