rails中的悲观锁和乐观锁

浏览: 187 发布日期: 2017-06-26 分类: ruby

并发

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。只有有资源的争用就少不了使用各种锁,包括关系数据库中使用的悲观锁和乐观锁,本文主要讲我们日常开发中很大可能会用到的两种锁.

常见的并发冲突

丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。

脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。

悲观锁和乐观锁

在平时开发使用中,我们经常使用两种思想来处理并发:
悲观锁: 悲观锁采用相对保守的策略,在资源争用比较严重的时候比较合适。悲观锁在事务开始之前就去尝试获得写权限,事务结束后释放锁;也就是说对于同一行记录,只有一个写事务可以并行;
乐观锁: 乐观锁是在提交事务之前,大家可以各自修改数据,但是在提交事务的时候,如果发现在这个过程中,数据发生了改变,那么直接拒绝此次事务提交。乐观锁适合在资源争用不激烈的时候使用。

悲观锁 (Pessimistic Lock)

Pessimistic Lock正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守悲观态度,事务每次去操作数据的时候都假设有其他事务会修改需要访问的数据,所以在访问之前都要求上锁,行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,因此,在整个数据处理过程中,将数据处于锁定状态。

悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能 真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系 统不会修改数据)。 一个典型的倚赖数据库的悲观锁调用: select * from account where name=”Erica” for update 这条sql 语句锁定了account 表中所有符合检索条件(name=”Erica”)的记录。 本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。

应用场景

一般对于资源的争用都可以使用悲观锁,比如电商系统中涉及到订单的部分,比如用户支付完成后可能会同时有多条支付成功的通知(做过支付的都知道一般有同步通知和异步通知),比如订单改价的同时可能用户正在支付等等,对于这种会对订单状态发生改变的操作,我们内部一般对这种操作都做加锁处理。

在Rails 的ActiveRecord 使用悲观锁

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

# select * from product where sku = '12343243' limit 1 for update
Product.where(sku: '12343243').lock(true).first
# 注意,这里可不是行锁,这里会是一个表锁

"select * from where xxx for update" 时,在 repeat read的隔离级别下,MySQL 加锁机制取决于sku的索引

  • 如果name没有索引,则锁全表。

  • 如果name 有普通索引,则锁一个区间 - range lock。

  • 如果 name 是唯一索引,仅仅锁一行。

  • 如果name 是主键,仅仅锁一行。

如果查询的条件没有落在索引上,最好不要这样来用。折中一下,我们不愿意在处理一条数据时把整张表都锁住,但是又没有办法直接找到数据行的id,可以这么做:

Product.transaction do
  # select * from products where ...
  products = Product.where(...).all
  product1 = products.detect { |account| ... }
  product2 = products.detect { |account| ... }
  # select * from products where id=? for update
  product1.lock!
  product2.lock!
  product1.stock -= 1
  product1.save!
  product2.stock += 1
  product2.save!
end

后来的版本里还提供了比较简便的写法

product = Product.find(1)
product.with_lock do 
  product.stock -= 1
  product.save!
end

乐观锁 (Pessimistic Lock)

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳

应用场景

对于同一个表单数据,有两个人同时在操作,第一个人先行保存了数据,第二个人要保存时,此时数据库存的数据已经不是他取出时的数据了,这个时候如果有乐观锁的机制,会在检查完数据版本后抛出异常.让第二个人意识到有其他人对该数据做了修改.

在Rails 的ActiveRecord 使用乐观锁

乐观锁本质上算是一个利用多版本管理来控制并发的技术,如果事务提交之后,数据库发现写入进程传入的版本号与目前数据库中的版本号不一致,说明有其他人已经修改过数据,不再允许本事务的提交。所以,使用乐观锁之前需要给数据库增加一列 :lock_version,Rails 会自动识别这一列,像数据库提交数据的时候自动带上。另外,乐观锁是默认打开的,如果要关闭,需要配置一下。如果事务提交失败,那么 Rails 会抛一个ActiveRecord::StaleObjectError 的异常。

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

p1.first_name = "Michael"
p1.save

p2.first_name = "should fail"
p2.save # Raises a 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

小结

悲观锁出错概率小,因为一旦获得锁,其他进程会堵塞,但是也导致速度会受影响,系统开销比较大,不利于并发。乐观锁适用于资源竞争不是那么多的地方,这样系统的开销较小,速度也比较快。

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

返回顶部