对 MySQL 三种锁(行级锁、表级锁、页级锁)与间隙锁的介绍,悲观锁与乐观锁的理解与抉择方案

行级锁、表级锁、页级锁、间隙锁
  1. 行级锁

    • 描述:行级锁是 MySQL 中颗粒度最小的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁颗粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁

    • 其实行级锁和页级锁之间还有其他颗粒度的锁,也就是间隙锁临键锁

      InnoDB 有三种行锁的算法

      • Record Lock(记录锁):单个行记录上的锁,这个也是我们日常认为的行锁。
      • Gap Lock(间隙锁):间隙锁,锁定一个范围,但不包括记录本身(只不过它的颗粒度比记录锁的整行更大了,它是锁定了某个范围内的多个行,包括根本不存在的数据)。GAP 锁的目的,是为了防止同一事物的两次当前读,出现幻读的情况。该锁只会在隔离级别是 RR 或者以上的级别内存在。间隙锁的目的是为了让其它事务无法在间隙中新增数据。
      • Next-Key Lock(临键锁):它是记录锁和间隙锁的结合,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。next-key 锁是 InnoDB 默认的锁。

      上面三种锁都是排它锁(X 锁)

    • next-key lock 的效果相当于一个记录锁加一个间隙锁。当 next-key lock 加在某索引上,则该记录和它前面的区间都被锁定

  2. 表级锁

    • 描述:表级锁是 MySQL 中的锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MyISAM 与 InnoDB 都支持表级锁定。表级锁定分别为表共享锁(共享锁)与表独占写锁(排他锁
    • LOCK TABLE my_table_name READ; 用读锁锁表,会阻塞其他事物修改表数据
    • LOCK TABLE my_table_name WRITE; 用写锁锁表,会阻塞其他事物读和写

    MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。

    但是在 InnoDB 中如果需要表锁就需要显式地声明了

  3. 页级锁

    • 描述:页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级锁冲突少,但速度慢。因此,采取了这种的页级锁,一次锁定相邻的一组记录,BDB 支持页级锁。
按照锁的共享策略来分:共享锁、排他锁、意向共享锁、意向排他锁

共享锁和排他锁在 MySQL 中具体体现就是读锁和写锁

  • 读锁(共享锁):Shared Locks(S 锁),针对同一份数据,多个读操作可以同时进行而不会互相影响
  • 写锁(排他锁):Exclusive Locks(X 锁),当前写操作没有完成前,它会阻塞其他写锁和读锁
  • IS 锁:意向共享锁、Intention Shared Lock。当食物准备在某条记录上加 S 锁时,需要先在表级别加上一个 IS 锁。
  • IX 锁:意向排他锁、Intention Exclusive Lock。当事务准备在某记录上加 X 锁时,需要先在表级别加上一个 IX 锁。

​ IS、IX 锁是表级锁,它们的提出仅仅为了在之后加表级别的 S 锁和 X 锁时可以快速判断表中的记录是否被加上锁,以避免用遍历的方式来查看表中有没有上锁的记录。就是说当对一个行加锁之后,如果有打算给行所在的表加一个表锁,必须先看看该表的行有没有被加锁,否则就会出现冲突。IS 和 IX 锁就避免了判断表中行有没有加锁对每一行的遍历。直接查看表有没有意向锁就可以知道表中有没有行锁

从加锁策略上分:乐观锁 / 悲观锁
  • 乐观并发控制 / 乐观锁(OCC - Optimistic Concurrency Control / Optimistic Locking)

  • 观并发控制 / 悲观锁(PCC - Pessimistic Concurrency Control / Pessimistic Lock)

悲观锁
  • 悲观锁理解

    • 在关系数据库管理系统中,悲观并发控制(悲观锁)是一种并发控制的方法;悲观锁指的是采用一种持悲观消极的态度,默认数据被外界访问时,必然会产生冲突,所以在数据处理的整个过程中都采用加锁的状态,保证在同一时间、只有一个线程可以访问到数据,实现数据的排他性;通常,数据库的悲观锁是利用数据库本身提供的锁机制去实现的
  • 悲观锁的实现

    • 外界要访问某条数据,那它就要首先向数据库申请改数据的锁(某种锁)

    • 如果获取成功,那它就可以操作改数据,在它操作期间,其它客户端就无法操作该数据了

    • 如果获取失败,则代表通一时间已有其它客户端获取了该锁,那就必须等待其他用户释放锁‘

  • 优缺点

    • 优点:

      • 适合在写多读少的并发环境中使用,虽然无法维持非常多的性能,但是在乐观锁无法提更好的性能前提下,可以做到数据的安全性
    • 缺点:

      • 加锁会增加系统开销,虽然能保证数据的安全,但是数据的吞吐量低,不适合在读多写少的场景下使用
乐观锁
  • 乐观锁的理解
    • 乐观锁是假设认为即使在并发环境中,外界对数据的操作一般是不会造成冲突,所以并不会去加锁(所以乐观锁并不是一把锁),而是在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回冲突信息,让用户决定如何去做下一步,比如说重试,知道成功为止;数据库的乐观锁,并不是利用数据库本身的锁去实现的,可能是利用某种实现逻辑去实现做到乐观锁的思想。
  • 乐观锁的实现:
    • 通常乐观锁的实现有两种,但他们内在都是 CAS 思想的设计:
      1. 方式一:使用数据版本(version)实现
        1. 这是乐观锁最常用的一种实现方式。什么事数据版本呢?就是在表中增加一个字段作为该记录的版本标识,比如叫 version,每次对该记录的写操作都会让 version + 1。
        2. 所以当我们读取了数据(包括 version),做出更新,要提交的时候,就会拿取得的 version 取跟数据库中的 version 比较是否一致,如果一致则代表这个时间段,并没有其它的县城也对这个数据进行修改、更新,同时 version + 1;如果不一致,则代表这个时间段,该记录已经被其它线程修改过了,认为是过期数据,返回冲突信息,比如重试(重新读取最新数据,再过更新)
        3. update table set num = num + 1, version = version + 1 where version = #{version} and id = #{id}
      2. 方式二:使用时间戳(timestamp)实现
        1. 表中增加一个字段,名称无所谓,比如叫 update_time,字段类型使用时间戳(timestamp)
        2. 原理和方式一致,也是在更新提交时,检查当前数据库中数据的时间戳和自己更新前取得的时间戳是否一致,如果一致则代表此刻没有冲突,可以提交更新,同时时间戳更新为当前时间,否则就是该时间段有其它线程也更新提交过,返回冲突信息,等待用户下一步动作。
        3. update table set num = num + 1, update_time = unix_timestamp(now()) where id = #{id} and update_time = #{updateTime}
    • 注意:实现乐观锁的时候,我们必须保证 CAS 多个操作的原子性,即获取数据库数据的版本,拿数据库版本与之前拿到的版本的比较,以及更新数据等这几个操作的执行必须是连贯执行,具有符合操作的原子性;所以如果是数据库的 SQL,那么我们就要保证多个 SQL 操作处于同一个事务中
  • 优缺点
    • 优点
      • 在读多写少的并发场景下,可以避免数据库加锁的开销,提高 DAO 层的响应性能
      • 其实很多情况下,我们 ORM 工具都带有乐观锁的实现,所以这些方法不一定需要我们认为去实现
    • 缺点
      • 在写多读少的并发场景下,即在写操作的竞争激烈的情况下,会导致 CAS 多次重试,冲突评率过高,导致开销比悲观锁更高
乐观锁和悲观锁的抉择

对乐观锁和悲观锁的抉择主要体现在写-写

在乐观锁的抉择中,我们可以从下面三个因素来考虑

  • 响应速度:如果 DAO 层需要非常高的响应速度,尤其是读多写少的场景下,那我们就可以采用乐观锁方案,降低数据库锁的开销,提供并发量
  • 冲突频率:如果冲突频率非常高,那么我们就可以采用悲观锁,保证成功率;毕竟如果冲突频率大,乐观锁会需要多次重试才能重试成功,代价可能会大大增加
  • 重试代价:如果重试代价大,比如说重试过程的代码执行非常耗时,那么此时就不建议使用乐观锁了,还不如直接上悲观锁爽快

所以我们知道

  • 在读多写少,CAS 竞争没那么激烈的时候,我们可以采用乐观锁策略,降低数据库加锁的开销,提高数据库并发响应
  • 在读多写少的情景下,因为会产生大量的 CAS 竞争,且重试成本比较高的情况下,我们就不建议再采用乐观锁策略了,还是直接使用悲观锁的数据库加锁吧

参考资料

【MySQL笔记】正确的理解MySQL的乐观锁,悲观锁与MVCC

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值