rails乐观锁使用

1818 words, 6 mins

rails 中关于乐观锁的使用非常简单,可以查看文档

使用

只需要在数据表添加lock_version这个字段就可以了。原理就是每一次update操作都会递增lock_version,如果有多个update同时对同一个lock_version的行进行更新,只要前一个更新成功,后一个再去更新就会抛异常StaleObjectError

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

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

可以指定锁定版本的字段名称

class Destination
   self.locking_column = "my_custom_locking"
end

例子:

p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.first_name = "should fail"
p2.save # Raises an ActiveRecord::StaleObjectError

删除的时候也会检查

p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.destroy # Raises an ActiveRecord::StaleObjectError

结合 web 前端,最好是在form表单提交的时候添加一个hidden field,名字就是lock_version,像这样

<%= form_for @destination do |form| %>
   <%= form.hidden_field :lock_version %>
   <%# ... other inputs %>
<% end %>

原理

更新语句构造的 sql会带着lock_version

UPDATE `orders` SET `leave_count` = 9, `updated_at` = '2018-07-15 06:47:28', `lock_version` = 1 WHERE `orders`.`id` = 1 AND `orders`.`lock_version` = 0

一旦lock_version已经被修改,更新语句影响的数据行就为 0,查看源码实现也可以验证:

# active_record/locking/optimistic.rb

def _update_row(attribute_names, attempted_action = "update")
  return super unless locking_enabled?

  begin
    locking_column = self.class.locking_column
    # 先获取 lock_version 的值
    previous_lock_value = read_attribute_before_type_cast(locking_column)
    attribute_names << locking_column

    self[locking_column] += 1
		# 带着 lock_version 去 update
    affected_rows = self.class._update_record(
      attributes_with_values(attribute_names),
      self.class.primary_key => id_in_database,
      locking_column => previous_lock_value
      )
		# 如果没 update 成功,也就是 affrected_rows == 0,则说明被别的并发修改抢占了,于是抛出异常
    if affected_rows != 1
      raise ActiveRecord::StaleObjectError.new(self, attempted_action)
    end

    affected_rows

    # If something went wrong, revert the locking_column value.
  rescue Exception
    self[locking_column] = previous_lock_value.to_i
    raise
  end
end