乐观锁指南 已翻译 100%

ifsc01 投递于 09/04 22:49 (共 6 段, 翻译完成于 11-13)
阅读 4198
收藏 28
1
加载中

这篇客座文章来自Viget Labs的社区贡献者和 Engine Yard 合作伙伴Brian Landau。Brian是Viget Labs的一名开发人员,他在那里经手大大小小的web应用程序的开发工作。他主要使用Ruby和JavaScript,但也喜欢尝试其他语言(Io是他目前最喜欢的)。

虽然乐观锁已经在Rails中存在很长时间了,但我发现我和我周围的人很少利用它。通过查看Rails API文档,您可以很容易地开始使用乐观锁,但是您很快就会发现,您需要做更多的事情,然后添加lock_version列来充分利用这个特性。

liyue李月
liyue李月
翻译于 10/11 17:46
0

开始

乐观锁的用例是防止用户覆盖其他用户所做的更改。假设Billy来到你的旅行网页应用程序,想要改变一个地点,但是就在他提交之前,Jenny过来提交了一个改变。乐观锁将阻止Billy的更改通过。从乐观锁开始实际上很简单,只需向要启用锁的每个表添加一个lock_version列即可。

class AddLockingColumns < ActiveRecord::Migration
   def self.up
      add_column :destinations, :lock_version, :integer
   end

   def self.down
      remove_column :destinations, :lock_version
   end
end

在添加这个列之后,对模型的每次更新都会导致这个锁版本被增加。如果由于某种原因,您不能使用列名lock_version,那么没问题:使用其他名称,然后在类中这样设置它:

class Destination
   self.locking_column = "my_custom_locking"
end

完成此操作后,如果两个人同时试图向模型提交更新,其中一人将引发ActiveRecord::StaleObjectError错误。

liyue李月
liyue李月
翻译于 10/11 17:49
0

修复edit  form_for

虽然这种行为是有益的,但它并不能解决更令人担忧的问题。假设Billy来改变同一个度假目的地。他打开编辑表单,猛敲内容,然后走开去喝咖啡。他心烦意乱,有几个小时都没回来工作。他不在的时候,Jenny迅速地换了一个地方。他返回并完成了内容更改并提交了编辑表单。会发生什么呢?好吧,在乐观锁定的情况下,他的修改成功并通过数据库。这对我来说是有问题的,不是我想要的。这是因为在更新操作中实例化模型对象时,从数据库中设置了lock_version。我们需要的是,当Billy访问编辑表单时,将模型锁定到他已有的版本。实现这一点的最佳方法是为lock_version字段添加一个隐藏输入。然后,当有人提交表单时,如果锁版本在被访问后增加了,那么更新将失败,出现ActiveRecord::StaleObjectError错误。你可以手动通过添加这个隐藏字段到每一个你需要锁定的表单上,就像这样:

<%= form_for @destination do |form| %>
   <%= form.hidden_field :lock_version %>
   <%# ... other inputs %>
<% end %>
liyue李月
liyue李月
翻译于 10/11 17:54
0

你也可以在程序中添加下面这段代码让你的生活更加轻松:

module ActionView
   module Helpers
      module OptimisticLockingFormFor

         def self.included(base)
            base.alias_method_chain :form_for, :optimistic_locking
         end

         def form_for_with_optimistic_locking(record_or_name_or_array, *args, &amp;block)
            form_for_without_optimistic_locking(record_or_name_or_array, *args) do |form_with_locking|
               lock_form = form_with_locking.object &&
                  form_with_locking.object.respond_to?(:locking_enabled?) &&
                  form_with_locking.object.locking_enabled? &&
                  !form_with_locking.object.new_record?
               if lock_form
                  concat(content_tag(:div,
                     form_with_locking.hidden_field(form_with_locking.object.class.locking_column),
                     :style => 'margin:0;padding:0;display:inline').html_safe)
               end
               yield form_with_locking
            end
         end

      end
   end
end

ActionView::Base.send :include, ActionView::Helpers::OptimisticLockingFormFor

此代码通过修改 form_for 从而自动为每个开启了锁功能的版本化对象添加“lock_version”隐藏输入(未版本化的新记录提交锁版本会出问题)。从此你不必惦记着为每个需要开启乐观锁的表单添加锁版本,真棒!现在 Billy 可以尽情喝咖啡而不用担心覆盖掉 Jenny 的修改了。我看到其他一些在用户访问编辑表单的时候把表单绑定到锁版本中的方法建议,例如把模型对象存入 session 或者把锁版本存入 session。但这些建议的方案是有缺陷的。在 session 中存储模型并不是一个好主意,其原因在其他的地方已经被很好地解释。总而言之,我相信在表单中保存锁版本是处理这类锁的最好方式,因为他确保了锁版本与用户所访问的表单是被绑定的。

Shawearn
Shawearn
翻译于 11/13 12:08
1

摆脱陈腐,融入新鲜事物

剩下的一个问题是,如果 Billy 在 Jenny 已经做了更改之后再做更改,就会出现 ActiveRecord::StaleObjectError 错误,默认情况下会导致空白页面。最简单的解决方案是在 public/409.html 中添加一个静态 HTML 错误页面。我更喜欢做的是捕捉错误,并弹出错误消息展示页面,告诉用户发生了什么。我们还希望确保记录:1)是最新版本,2)没有使用来自原始编辑的锁版本,3)使用用户在表单中输入的属性值。这样做的原因是为了确保表单具有正确的锁版本,并具有用户提供的值,以便他们能够轻松地重新提交表单。你可以在控制器动作中这样做:

class DestinationsController < ApplicationController
   def update
      # ... update code
   rescue ActiveRecord::StaleObjectError
      @destination.reload.attributes = params[:destination].reject do |attrb, value|
         attrb.to_sym == :lock_version
      end
      flash.now[:error] = "Another user has made a change to that record "+
         "since you accessed the edit form."
      render :edit, :status => :conflict
   end
end

随着你的应用程序规模的增长,你可能会想要使用类似这样的东西来做到拦截ActiveRecord::StaleObjectError:

class ApplicationController < ActionController::Base
   rescue_from ActiveRecord::StaleObjectError do |exception|
      respond_to do |format|
         format.html {
            correct_stale_record_version
            stale_record_recovery_action
        }
        format.xml  { head :conflict }
        format.json { head :conflict }
     end
  end      

   protected   

   def stale_record_recovery_action
      flash.now[:error] = "Another user has made a change to that record "+
         "since you accessed the edit form."
      render :edit, :status => :conflict
   end
end

class DestinationsController < ApplicationController
   protected

   def correct_stale_record_version
      @destination.reload.attributes = params[:destination].reject do |attrb, value|
         attrb.to_sym == :lock_version
      end
   end
end

您还会注意到,在 rescue_from 块中,XML 和 JSON API 请求都是通过返回一个 409 即”Conflict”状态代码来处理的。

雪落无痕xdj
翻译于 11/07 21:59
0

结论

乐观锁定并不是在所有场景下都适用;有些应用程序不需要任何类型的锁,有些却需要更严格形式的锁。你可能还会发现你需要从各类冲突中恢复过来。例如,您可能希望在用户试图提交的版本后,再在数据库中显示当前的版本。无论如何考虑到大部分应用程序用例的情况下,乐观锁还是容易实现的,没有理由不去试用下。

ZICK_ZEON
ZICK_ZEON
翻译于 10/08 22:21
0
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(2)

f
freezingsky
redis上玩乐观锁的,都是挺有想法的
SupNatural
SupNatural
沙发。我已经乐观到懒得用锁了。
返回顶部
顶部