美文网首页
Rails 锁(未完成)

Rails 锁(未完成)

作者: paul_deng | 来源:发表于2023-02-26 16:32 被阅读0次

    1. 引言

    1.1 为什么需要锁(并发控制)?

    在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。

    典型的冲突有

    • 丢失更新
      一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新。
    • 脏读
      当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6。

    1.2 锁一般分为哪几种?

    • 悲观锁(Pessimistic Lock)
      ++假定会发生并发冲突++,屏蔽一切可能违反数据完整性的操作。
      Java 中的 synchronized,如果某个资源定义为 synchronized,那么该资源的调用只能排队,一个使用完成后,另外一个才能开始使用。

    • 乐观锁(Optimistic Lock)
      ++假设不会发生并发冲突++,只在提交操作时++检查++是否违反数据完整性。 乐观锁不能解决脏读的问题。
      Java JUC中的atomic (原子) 包就是乐观锁的一种实现,AtomicInteger 通过CAS(compare-and-swap)操作实现线程安全的自增。

    2. 悲观锁

    2.1 概述

    悲观锁 ++假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的++,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且++直到你提交了所作的更改之后才释放锁++。

    悲观的缺陷 是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会++长时间的锁定一个对象,限制其他用户的访问++,也就是说++悲观锁的并发访问性不好++。

    悲观锁,主要依赖的是数据库的排他锁来实现

    2.2 SQL 实现

    实现悲观锁,需要以下两步:

      1. 使用 transaction,当 commit/rollback 时释放锁。
      1. 使用 FOR UPDATE 请求对查询的资源进行加锁。

    我们使用 SQL 来做一个试验 (基于PostgreSQL),试验假设的内容:

    • 第一步,【在查询窗口1】使用悲观锁对资源进行加锁。
    • 第二步,【在查询窗口1】使用 sleep 模拟业务处理的等待。
    -- 在窗口1先执行以下代码(自行保证数据存在):
    BEGIN;
    SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
    SELECT pg_sleep(10);
    COMMIT;
    
    • 第三步,【在查询窗口2】在另外一个窗口请求对相同资源的访问。分别测试了以下四个情况:

    情景 1. 不使用悲观锁对资源进行访问:

    SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88'
    -- 结果:不需要等待窗口1完成,直接输出结果
    

    情景 2. 使用悲观锁进行请求访问:

    SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
    -- 结果:需要等待窗口1完成后,才输出结果
    

    情景 3. 使用悲观锁进行请求访问:

    SELECT * FROM charge_stations WHERE id = '0404de7c-ccc5-426f-b1ec-8effa9a31a88' FOR UPDATE;
    -- 结果:需要等待窗口1完成后,才输出结果
    
    -- 注意:窗口1的记录,它的serial_number是10027
    SELECT * FROM charge_stations WHERE serial_number LIKE '1002%' FOR UPDATE;
    -- 结果:需要等待窗口1完成后,才输出结果
    

    情景 4. 使用悲观锁进行其它资源访问:

    -- 需要保证资源存在,才能达到测试的目的
    SELECT * FROM charge_stations WHERE serial_number = '10000' FOR UPDATE;
    -- 结果:不需要等待窗口1完成,直接输出结果
    

    ==综合可以得出,只有都在使用悲观锁,而且是对相同的资源,才会导致后面的访问等待。==

    2.3 Rails 的悲观锁实现 (程序层面使用悲观锁)

    锁方法

    相关方法有:lock、lock! 和 with_lock.
    其中,lock 和 with_lock 都是封装 lock! 而来。

    https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html

    方法一:lock

    为 ActiveRecord Relation 提供锁请求。

    用例:

    Account.lock.find(1)
    -- Output: select * from accounts where id=1 for update
    

    完整的锁应用例子:

    # 需要手动启动事务 和 加锁,不过 lock可以加参数,设置不同的lock模式,如: 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'
    Account.transaction do
      # select * from accounts where name = 'shugo' limit 1 for update
      shugo = Account.where(name: 'shugo').lock(true).first
      yuko = Account.where(name: 'yuko').lock(true).first
      shugo.balance -= 100
      shugo.save!
      yuko.balance += 100
      yuko.save!
    end
    

    方法二:with_lock

    自动启动事务,和 请求加锁

    用例:

    account = Account.find_by(name: 'shugo')
    account.with_lock do
      # This block is called within a transaction,
      # account is already locked.
      account.balance -= 100
      account.save!
    end
    

    3. 乐观锁

    3.1 概述

    乐观锁认为其他用户企图改变你正在更改的对象的概率是很小的,因此++乐观锁直到你准备提交所作的更改时才将对象锁住++,当你++读取以及改变该对象时并不加锁++。

    可见乐观锁++加锁的时间要比悲观锁短++,乐观锁可以++用较大的锁粒度获得较好的并发访问性能++。

    脏读导致的失败甚至要重置问题:如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,++会增加并发用户读取对象的次数++。

    乐观锁是一种并发解决的思想,是通过程序层面 + 数据库字段协同实现的。

    3.2 SQL 实现

    在数据库层面,主要有两种方式协助实现乐观锁:

    • 增加 version 字段
      Integer 类型,每次修改都增加1;修改当前记录时需要比较 version 与 读取的记录的 version 是否还保持一致。
    • 增加 timestamp 字段(名字可以叫 updated_time)
      每次记录更新后,都更新该字段为当前时间;修改当前记录时需要比较 timestamp 与 读取的记录的 timestamp 是否还保持一致。

    3.3 Rails 的乐观锁实现

    https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html

    4. 锁的应用场景

    从数据库厂商的角度看,使用乐观的页锁是比较好的,尤其在影响很多行的批量操作中可以放比较少的锁,从而降低对资源的需求提高数据库的性能。再考虑聚集索引。在数据库中记录是按照聚集索引的物理顺序存放的。如果使用页锁,当两个用户同时访问更改位于同一数据页上的相邻两行时,其中一个用户必须等待另一个用户释放锁,这会明显地降低系统的性能。interbase和大多数关系数据库一样,采用的是乐观锁,而且读锁是共享的,写锁是排他的。可以在一个读锁上再放置读锁,但不能再放置写锁;你不能在写锁上再放置任何锁。锁是目前解决多用户并发访问的有效手段。

    综上所述:在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题;
    但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法.
    

    5. 并发锁问题

    6. 数据库锁

    https://blog.csdn.net/puhaiyang/article/details/72284702

    https://www.cnblogs.com/deliver/p/5730616.html

    AASM lock。
    https://github.com/aasm/aasm/blob/master/README.md

    相关文章

      网友评论

          本文标题:Rails 锁(未完成)

          本文链接:https://www.haomeiwen.com/subject/kvrcldtx.html