Ruby、Rails知识Ruby on RailsRuby

rails中乐观锁和悲观锁的使用

2018-12-18  本文已影响1人  小新是个程序媛

MySQL乐观锁和悲观锁的介绍可以参考之前的一篇文章
MySQL中的锁(行锁,表锁,乐观锁,悲观锁,共享锁,排他锁)

同时补充下,在悲观锁中,对于如下语句的加锁机制,解释如下

"select * from where xxx for update" 

在 repeat read的隔离级别下,MySQL 加锁机制取决于name的索引
如果name没有索引,则锁全表。
如果name 有普通索引,则锁一个区间 - range lock。
如果 name 是唯一索引,仅仅锁一行。
如果name 是主键,仅仅锁一行。

今天主要介绍一下锁在rails中的使用

1. 悲观锁的使用

# select * from accounts where id=1 for update
Account.lock.find(1)
# 注意,这种最终会导致一个行锁

# select * from accounts where name = 'shugo' limit 1 for update
Account.where("name = 'shugo'").lock(true).first
# 注意,这里可能不是行锁,行锁 表锁取决于name字段的属性

Rails 也提供了一个很方便的方法 with_lock'来锁住单个记录,并且内嵌在事务之中。下面代码中的两段是等价的:

account = Account.find(1)
Account.transaction do
    account.lock!
    account.balance -= 100
    account.save! 
end

# 和下面是等价的
account.with_lock do
    account.balance -= 100
    account.save!
end

2. 乐观锁的使用

使用乐观锁之前需要给数据库增加一列 lock_version字段给相应的表,Rails 会自动识别这一列,像数据库提交数据的时候自动带上,如果事务提交失败,那么 Rails 会抛一个 ActiveRecord::StaleObjectError 的异常。
另外,乐观锁是默认打开的,如果要关闭,需要配置一下。

在大鱼系统中,库存管理是使用乐观锁的,流量没那么大,不太可能多个用户同时预订同一个住宿的同一个间夜,概率比较小,所以目前是使用乐观锁来实现的。如果抛异常,那么还可以进行重试。

比如,下面这段代码会进行重试:

retry_times = 3

begin
    @order.with_lock do
        @order.set_paid!
    end
rescue ActiveRecord::StaleObjectError => e
    retry_times -= 1
    if retry_times > 0
        retry
    else
        raise e
    end
rescue => e
    raise e
end

需要注意的地方

  1. 一般,使用锁的时候和事务同时使用,所以 with_lock 是用的比较多的,而且尽量使用行锁而不是表锁。
  2. 另外,也注意异常的处理,需要使用那些会抛异常的方法;
  3. 对于乐观锁,还需要注意如果是前端操作频繁,那么还需要把 lock_version 写入到 form 表单中,否则起不到锁的作用,这里讲的很详细了

参考:https://ruby-china.org/topics/28963

附录:使用乐观锁的示例:

terminal

rails g migration add_lock_version_to_products lock_version:integer
rake db:migrate

products/_form.html.erb

<%= f.hidden_field :lock_version %>

products_controller.rb

class ProductionsController < ApplicationController
   def update
      # ... update code
   rescue ActiveRecord::StaleObjectError
      @production.reload.attributes = params[:production].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
上一篇下一篇

猜你喜欢

热点阅读