0


[MySQL进阶]——深入了解锁

锁的分类

image-20220128223907604

操作类型

1) 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。

2) 写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。

锁的粒度

表级锁

  1. 表级别的S锁、X锁LOCK TABLES t READ :InnoDB存储引擎会对表 t 加表级别的 S锁 。LOCK TABLES t WRITE :InnoDB存储引擎会对表 t 加表级别的 X锁 。> 不推荐使用
  2. 意向锁- 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)- 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)> 意向锁之间互不排斥,但除了 IS 与 S 兼容外, 意向锁会与 共享锁 / 排他锁 互斥 。> > IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。作用:如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。意向锁的作用,相当于就是在低层次资源是否使用,加了一个tag来标识而已,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。
  3. 自增锁(AUTO-INC锁)AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放。那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。但是, AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。因此, 在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。一样也是在插入数据的时候,会为被 AUTO_INCREMENT 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。> 并发时,使用轻量级的自增锁,可能会导致自增长的值不是连续的。
  4. 元数据锁(MDL锁)当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写 锁。

行级锁

  1. 记录锁(Record Locks)

记录锁也就是仅仅把一条记录锁上。

记录锁是有S锁和X锁之分的,称之为 S型记录锁 和 X型记录锁 。

  1. 间隙锁(Gap Locks)间隙锁,锁定一个范围,但是不包含记录本身作用:- 防止幻读,以满足相关隔离级别的要求;事务在第一次执行读取操作时,那些幻影记录上尚未存在,无法给这些幻影记录加锁,于是提出了间隙锁。- 满足恢复和复制的需要:MySQL 的 Binlog 是按照事务提交的先后顺序记录的, 恢复也是按这个顺序进行的。由此可见,MySQL 的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读。
  2. 临键锁(Next-Key Locks)Next-Key Locks是行锁与间隙锁的组合(同时锁住数据+间隙锁)。

页级锁

页锁就是在 页的粒度 上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录

页锁的开销 介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

对待锁的态度

乐观锁

假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 乐观锁不能解决脏读的问题。

数据表中的实现

用数据版本号(version)机制是乐观锁最常用的一种实现方式。一般通过为数据库表增加一个数字类型的 “version” 字段,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1

当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,返回更新失败。

//step1: 查询出商品信息 
select (quantity,version) from items where id=100; 
//step2: 根据商品信息生成订单 
insert into orders(id,item_id) values(null,100); 
//step3: 修改商品的库存 update items set quantity=quantity-1,version=version+1 where id=100 and version=#{version};

悲观锁

假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。’

select…for update是MySQL提供的实现悲观锁的方式,在MySQL中用悲观锁务必须确定走了索引,而不是全表扫描,否则将会将整个数据表锁住

按加锁的方式划分

隐式锁

当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。隐式锁是InnoDB实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。

隐式锁主要用在插入场景中。

在Insert语句执行过程中,必须检查两种情况,一种是如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的,另一中情况如果Insert的记录和已有记录存在唯一键冲突,此时也不能插入记录。

insert语句的锁都是隐式锁,但跟踪代码发现,insert时并没有调用lock_rec_add_to_queue函数进行加锁, 其实所谓隐式锁就是在Insert过程中不加锁。

只有在特殊情况下,才会将隐式锁转换为显示锁。这个转换动作并不是加隐式锁的线程自发去做的,而是其他存在行数据冲突的线程去做的。例如事务1插入记录且未提交,此时事务2尝试对该记录加锁,那么事务2必须先判断记录上保存的事务id是否活跃,如果活跃则帮助事务1建立一个锁对象,而事务2自身进入等待事务1的状态

显示锁

通过特定的语句进行加锁,我们一般称之为显示加锁

select....lockinsharemodeselect....forupdate

其他锁

全局锁

全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候,可以使用这个命令,之后 其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结 构等)和更新类事务的提交语句。全局锁的典型使用 场景 是:做 全库逻辑备份 。

死锁

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。死 锁示例:

image-20220516191852507

这时候,事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互 相等待对方的资源释放,就是进入了死锁状态。

当出现死锁以后,有 两种策略 :

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级 排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on ,表示开启这个逻辑。

说了那么锁,但大家一定要注意的是刚才说的那些锁只是划分维度不同,比如说记录锁也可能是共享锁也可能是排他锁。

如何上锁

image-20220516194939082

行级锁加锁分析

mysql中比较关键的是行锁,接下来我们重点分析一下行锁

锁加在索引上

InnoDB的行锁是通过给索引上的索引项加锁来实现的. 如果SQL语句未命中索引,则走聚簇索引的全表扫描,表上每条记录都会上锁,导致并发能力下降,增大死锁的概率,因此需要为表合理的添加索引,线上查询尽量命中索引

即使在建表的时候没有指定主键,InnoDB会默认创建一个DB_ROW_ID的自增字段为表的主键,并且其主键索引(聚簇索引)为GEN_CLUST_INDEX
主键索引也被称为聚簇索引

可以看下面例子,涉及到回表对聚簇索引的索引项也会加锁:
在这里插入图片描述

加锁规则

  1. 原则1:加锁的基本单位是next-key lock。希望你还记得,next-key lock是前开后闭区间
  2. 原则2:查找过程中访问到的对象才会加锁
  3. 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
  4. 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
  5. .一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止

具体分析:

表t的建表语句

CREATETABLE`t`(`id`int(11) NOTNULL,`c`int(11) DEFAULTNULL,`d`int(11) DEFAULTNULL,PRIMARYKEY(`id`),KEY`c`(`c`))ENGINE=InnoDB;insertinto t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25)

等值查询间隙锁

image-20220516200629303

由于表t中没有id=7的记录,所以用我们上面提到的加锁规则判断一下的话:

  1. 根据原则1,加锁单位是next-key lock,session A加锁范围就是(5,10];
  2. 同时根据优化2,这是一个等值查询(id=7),而id=10不满足查询条件,next-key lock退化成间 隙锁,因此最终加锁的范围是(5,10)。

所以,session B要往这个间隙里面插入id=8的记录会被锁住,但是session C修改id=10这行是可 以的。

非唯一索引等值锁

image-20220516200723228

  1. 根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock。
  2. 要注意c是普通索引,因此仅访问c=5这一条记录是不能马上停下来的,需要向右遍历,查到 c=10才放弃。根据原则2,访问到的都要加锁,因此要给(5,10]加next-key lock。
  3. 但是同时这个符合优化2:等值判断,向右遍历,最后一个值不满足c=5这个等值条件,因此 退化成间隙锁(5,10)。
  4. 根据原则2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索 引,所以主键索引上没有加任何锁,这就是为什么session B的update语句可以执行完成。

但session C要插入一个(7,7,7)的记录,就会被session A的间隙锁(5,10)锁住。

需要注意,在这个例子中,lock in share mode只锁覆盖索引,但是如果是for update就不一样 了。 执行 for update时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。

这个例子说明,锁是加在索引上的;同时,它给我们的指导是,如果你要用lock in share mode 来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,在查询字段中加入索引中不 存在的字段。

主键索引范围锁

image-20220516200902918

  1. 开始执行的时候,要找到第一个id=10的行,因此本该是next-key lock(5,10]。 根据优化1, 主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁。
  2. 范围查找就往后继续找,找到id=15这一行停下来,因此需要加next-key lock(10,15]。

所以,session A这时候锁的范围就是主键索引上,行锁id=10和next-key lock(10,15]。这 样,session B和session C的结果你就能理解了。

这里你需要注意一点,首次session A定位查找id=10的行的时候,是当做等值查询来判断的,而 向右扫描到id=15的时候,用的是范围查询判断。

非唯一索引范围锁

image-20220516200955942

在第一次用c=10定位记录的时 候,索引c上加了(5,10]这个next-key lock后,由于索引c是非唯一索引,没有优化规则,也就是 说不会蜕变为行锁,因此最终sesion A加的锁是,索引c上的(5,10] 和(10,15] 这两个next-key lock。

所以从结果上来看,sesson B要插入(8,8,8)的这个insert语句时就被堵住了。 这里需要扫描到c=15才停止扫描,是合理的,因为InnoDB要扫到c=15,才知道不需要继续往后 找了。

总结

唯一索引等值查询:

  • 当查询的记录是存在的,next-key lock 会退化成「记录锁」。
  • 当查询的记录是不存在的,next-key lock 会退化成「间隙锁」。

非唯一索引等值查询:

  • 当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
  • 当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。

非唯一索引和主键索引的范围查询的加锁规则不同之处在于:

  • 唯一索引在满足一些条件的时候,next-key lock 退化为间隙锁和记录锁。
  • 非唯一索引范围查询,next-key lock 不会退化为间隙锁和记录锁。

参考链接:

我做了一天的实验!

标签: mysql 数据库

本文转载自: https://blog.csdn.net/weixin_65349299/article/details/124807401
版权归原作者 一定会去到彩虹海的麦当 所有, 如有侵权,请联系我们删除。

“[MySQL进阶]——深入了解锁”的评论:

还没有评论