MySQL锁汇总,一文搞懂InnoDB所有的锁

奋斗吧
奋斗吧
擅长邻域:未填写

标签: MySQL锁汇总,一文搞懂InnoDB所有的锁 Html/CSS博客 51CTO博客

2023-07-23 18:24:16 129浏览

MySQL锁汇总,一文搞懂InnoDB所有的锁,锁住的不是record一行记录,也不是column字段,那么InnoDB的行锁锁住的到底是什么呢?在上面三个案例中,我们分析一下它们的差异



文章目录

  • 一、MySQL InnoDB锁的基本类型
  • 1、锁的粒度
  • 2、锁的类型
  • (1)插入意向锁(Insert Intention Locks)(不常见)
  • (2)自增锁(AUTO-INC Locks)(不常见)
  • (3)Predicate Locks for Spatial Indexes(不常见)
  • 3、共享锁(Shared Locks)(行锁)
  • 4、排它锁(Exclusive Locks)(表锁)
  • 5、意向锁(Intention Locks)(表级锁)
  • 6、拓展-元数据锁(meta data lock,MDL) (表锁)
  • 7、抛出问题
  • 二、行锁的原理
  • 1、没有索引的表
  • 2、有主键索引的表
  • 3、有唯一索引的表
  • 4、小总结
  • 5、为什么没有索引,锁一行数据会导致锁整表?
  • 6、为什么通过唯一索引给数据行加锁,主键索引也会被锁住?
  • 三、行锁的算法
  • 1、概述
  • 2、记录锁(Record Locks)
  • 3、间隙锁(Gap Locks)
  • 4、临键锁(Next-Key Locks)
  • 5、小结:隔离级别的实现
  • 6、事务隔离级别的选择


一、MySQL InnoDB锁的基本类型

1、锁的粒度

我们知道,MyISAM只支持表锁,使用lock table的语法加锁:

lock tables xxx read;
lock tables xxx write;
unlock tables;

而InnoDB同时支持表锁和行锁。表锁和行锁的区别在哪?
锁定粒度:表锁 > 行锁
加锁效率: 表锁 > 行锁
冲突概率:表锁 > 行锁
并发性能:表锁 < 行锁

2、锁的类型

官方文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

MySQL锁汇总,一文搞懂InnoDB所有的锁_主键


我们可以看到,官网把锁分成了8类。我们把前面的两个行级别的锁(Shared and Exclusive Lokes),和两个表级别的锁(Intention Locks)称为锁的基本模式。

后面三个Record Locks、Gap Locks、Next-Key Locks,我们把它们叫做锁的算法,也就是分别在什么情况下锁定什么范围。

剩下的三个分别是插入意向锁(Insert Intention Locks)、自增锁(AUTO-INC Locks)、索引锁(Predicate Locks for Spatial Indexes)。

(1)插入意向锁(Insert Intention Locks)(不常见)

是一个特殊的间隙锁。间隙锁不允许插入数据,但是插入意向锁允许多个事务同时插入数据到同一个范围。比如(4,7),一个事务插入5,一个事务插入6,不会发生锁等待。

(2)自增锁(AUTO-INC Locks)(不常见)

是一种特殊的表锁,用来防止自增字段重复,数据插入以后就会释放,不需要等到事务提交才释放。如果需要选择更快的自增值生成速度或者更加连续的自增值,就要通过修改自增锁的模式改变:

show variables like 'innodb_autoinc_lock_mode';

0:traditional(每次都会产生表锁)。
1:consecutive(会产生一个轻量锁,simple insert会获得批量的锁,保证连续插入,默认值)。
2:interleaved(不会锁表,来一个处理一个,并发最高)。

(3)Predicate Locks for Spatial Indexes(不常见)

是5.7版本里面新增的一种数据类型的索引的锁。

3、共享锁(Shared Locks)(行锁)

我们获取了一行数据的读锁以后,可以用来读取数据,所以它也叫做读锁,注意不要在加上了读锁以后去写数据,不然的话可能会出现死锁的情况。而且多个事务可以共享一把读锁。

共享锁的作用:因为共享锁会阻塞其他事务的修改,所以可以用在不允许其他事务修改数据的情况(共享锁和写锁互斥的例子后面会有)。

那么怎么给一行数据加上读锁呢?我们可以用select … lock in share mode; 的方式手动加上一把读锁。

释放锁有两种方式,只要事务结束,锁就会自动释放,包括提交事务和结束事务。

我们验证一下,看共享锁是不是可以重复获取:

MySQL锁汇总,一文搞懂InnoDB所有的锁_数据_02

4、排它锁(Exclusive Locks)(表锁)

排它锁是用来操作数据的,所以又叫做写锁。只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数据的共享锁和排它锁。

排它锁的加锁方式有两种:
一种是自动加排它锁,我们在操作数据的时候,包括增删改,都会默认加上一个排它锁。
还有一种是手动加锁,我们用一个for update给一行数据加上一个排它锁,这个无论是在我们代码里面还是操作数据的工具里面,都比较常用。

释放锁有两种方式,只要事务结束,锁就会自动释放,包括提交事务和结束事务。

排它锁的验证:

MySQL锁汇总,一文搞懂InnoDB所有的锁_数据库_03

5、意向锁(Intention Locks)(表级锁)

意向锁是由数据库自己维护的。

也就是说,当我们给一行数据加上共享锁之前,数据库会自动在这张表上面加一个意向共享锁
当我们给一行数据加上排它锁之前,数据库会自动在这张表上面加一个意向排它锁

反过来也就是说,如果一张表上面至少有一个意向共享锁,说明有其他的事务给其中的某些数据行加上了共享锁。
如果一张表上面至少有一个意向排它锁,说明有其他事务给其中的某些数据行加上了排它锁。

MySQL锁汇总,一文搞懂InnoDB所有的锁_加锁_04


意向锁跟意向锁是不冲突的,意向锁跟行锁也不冲突

那么这两个表级别的锁存在的意义是什么呢?

如果说没有意向锁的话,当我们准备给一张表加上表锁的时候,我们首先要做什么?是不是必须先要去判断有没有其他事务锁定了其中的某些行?如果有的话,肯定不能加上表锁。那么这个时候我们就要去扫描整张表才能确定能不能成功加上一个表锁,如果数据量特别大,比如有上千万的数据的时候,加表锁的效率是不是很低?

但是我们引入了意向锁之后就不一样了。我只要判断这张表上面有没有意向锁,如果有,就直接返回失败。如果没有,就可以加锁成功。所以InnoDB里面的表锁,我们可以把它理解成一个标志。就像火车上卫生间有没有人使用的灯,让你不用去推门,是用来提高加锁的效率的。

MySQL锁汇总,一文搞懂InnoDB所有的锁_mysql_05

6、拓展-元数据锁(meta data lock,MDL) (表锁)

mysql创建索引导致死锁,数据库崩溃,mysql的表级锁之【元数据锁(meta data lock,MDL)】全解

navicat下对mysql创建索引导致死锁,数据库崩溃,完美解决方案

7、抛出问题

锁的作用是什么?它跟Java里面的锁是一样的,是为了解决资源竞争的问题,Java里面的资源是对象,数据库的资源就是数据表或者数据行。

所以锁是用来解决事务对数据的并发访问的问题的

那么,锁到底锁住了什么呢?当一个事务锁住了一行数据的时候,其他事务不能操作这一行数据,那它到底是锁住了这一行数据,还是锁住了这一个字段,还是锁住了别的什么东西呢?

二、行锁的原理

1、没有索引的表

首先我们有三张表,一张没有索引的t1,一张有主键索引的t2,一张有唯一索引的t3。

MySQL锁汇总,一文搞懂InnoDB所有的锁_数据库_06


我们在两个会话里面手动开启两个事务。

第一个事务里,通过where id = 1加锁;第二个事务里,尝试给id = 3的数据加锁,发现是阻塞住了,插入id=5的数据也被阻塞。

实际上,在没有索引或者没有用到索引的情况下,会锁整表
所以我们推断出,InnoDB的行锁锁住的不是一行记录(Record)

2、有主键索引的表

t2表在主键id上增加了索引,里面的数据是1、4、7、10:

MySQL锁汇总,一文搞懂InnoDB所有的锁_数据_07


第一次使用相同的id去加锁,冲突了;使用不同的id去加锁,可以加锁成功。

我们上面推断出,行锁锁住的并不是一行,那么会不会是锁住的id这个字段呢?

3、有唯一索引的表

t3表,我们在id上创建了主键索引,name上创建了唯一索引。里面的数据是1、4、7、10:

MySQL锁汇总,一文搞懂InnoDB所有的锁_mysql_08


第一个事务,我们通过name字段去锁定一行记录。

第二个事务,第一次尝试获取一样的排它锁,是阻塞的。

我们换了个字段id 来去锁定这行记录,发现仍然是阻塞的!说明行锁锁住的并不是字段。否则就不会出现第一个事务锁住了name,第二个字段锁住id失败的情况。

4、小总结

通过以上推断,我们得出结论:锁住的不是record一行记录,也不是column字段,那么InnoDB的行锁锁住的到底是什么呢?在上面三个案例中,我们分析一下它们的差异在哪里,这三张表结构的什么区别导致了加锁行为的差异?

答案就是索引。InnoDB的行锁,就是通过锁住索引来实现的

5、为什么没有索引,锁一行数据会导致锁整表?

为什么表里面没有索引的时候,锁住一行数据会导致锁整表?或者说,如果锁住的是索引,一张表没有索引怎么办?所以,一张表有没有可能没有索引?
(1)如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择主键作为聚集索引。
(2)如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引。
(3)如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐藏的聚集索引,它会随着行记录的写入而逐渐递增。

所以,没有使用索引时为什么锁表,是因为查询没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索引都锁住了

6、为什么通过唯一索引给数据行加锁,主键索引也会被锁住?

在辅助索引里面,索引存储的是二级索引和主键的值。比如name=4,存储的是name的索引和主键id的值4。

而主键索引里面除了索引之外,还存储了完整的数据。所以我们通过辅助索引锁定一行数据的时候,它跟我们检索数据的步骤是一样的,会通过主键值找到主键索引,然后也锁定。

本质上是因为锁定的是同一行数据,是相互冲突的。

MySQL锁汇总,一文搞懂InnoDB所有的锁_mysql_09

三、行锁的算法

1、概述

我们准备一张测试表t2,表中有一个主键索引,有四条数据,主键id分别是1、4、7、10。

因为我们用主键索引加锁,我们这里的划分标准就是主键索引的值。

MySQL锁汇总,一文搞懂InnoDB所有的锁_数据库_10


这些数据库里面存在的主键值,我们把它叫做Record,记录,那么我们这里就有4个Record。

根据主键,这些存在的Record隔开的数据不存在的区间,我们把它叫做Gap,间隙,它是一个左开右开的区间。

假设我们有N个Record,那么所有的数据会被划分成多少个Gap区间?答案是N+1,就像我们把一条绳子砍N刀,它最后肯定是变成N+1段。

间隙(Gap)连同它左边的记录(Record),我们把它叫做临键的区间,它是一个左开右闭的区间。

整形的主键索引,它是可以排序的,所以才有这种区间。如果我们的主键索引不是整形,是字符怎么办呢?其实任何一个字符集,都有相应的排序规则

MySQL锁汇总,一文搞懂InnoDB所有的锁_数据_11

2、记录锁(Record Locks)

当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精确匹配到一条记录的时候,这个时候使用的就是记录锁。

MySQL锁汇总,一文搞懂InnoDB所有的锁_加锁_12

比如where id = 1、4、7、10 。
我们使用不同的key去加锁,不会冲突,它只锁住这个record。

3、间隙锁(Gap Locks)

当我们查询的记录不存在,没有命中任何一个record,无论是用等值查询还是范围查询的时候,它使用的都是间隙锁

MySQL锁汇总,一文搞懂InnoDB所有的锁_数据库_13


MySQL锁汇总,一文搞懂InnoDB所有的锁_主键_14


注意!间隙锁主要是阻塞插入insert,相同的间隙锁之间不冲突。

4、临键锁(Next-Key Locks)

当我们使用了范围查询,不仅仅命中了Record记录,还包含了Gap间隙,在这种情况下,我们使用的就是临键锁,它是MySQL里面默认的行锁算法,相当于记录锁加上间隙锁。

唯一性索引,等值查询匹配到一条记录的时候,退化成记录锁。
没有匹配到任何记录的时候,退化成间隙锁。

MySQL锁汇总,一文搞懂InnoDB所有的锁_加锁_15


MySQL锁汇总,一文搞懂InnoDB所有的锁_mysql_16


临键锁,锁住最后一个key的下一个左开右闭的区间。

select * from t2 where id > 5 and id <=7 for update; -- 锁住(4,7]和(7,10]
select * from t2 where id > 8 and id <=10 for update; -- 锁住(7,10],(10, +无穷)

5、小结:隔离级别的实现

所以,我们再看事务隔离级别,为什么InnoDB的RR级别能够解决幻读的问题,就是用临键锁实现的。

事务隔离级别

脏读

不可重复读

幻读

未提交读(Read Uncommitted)

可能

可能

可能

已提交读(Read Committed)

不可能

可能

可能

可重复读(Repeatable Read)

不可能

不可能

对InnoDB不可能

串行化(Serializable)

不可能

不可能

不可能

  • Read uncommited:RU隔离级别,不加锁。
  • Serializable:所有的select都会隐式的转化为select … in share mode,会和update、delete互斥。
  • Repeatable Read:RR隔离级别下,普通的select使用快照读(Snapshot read),底层使用MVCC来实现。加锁的select(select … in share mode / select … for update)以及更新操作update、delete等语句使用当前读(current read),底层使用记录锁、间隙锁、临键锁
  • Read Commited:RC隔离级别下,普通的select都是快照读,使用MVCC实现。加锁的select都使用记录锁,因为没有Gap Lock。所以RC会出现幻读问题。

除了两种特殊情况:外键约束检查(Foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用间隙锁封锁区间。

6、事务隔离级别的选择

RU和Serializable肯定不能用。为什么有些公司要用RC,或者说网上有些文章推荐RC?

RC和RR主要有几个区别:
1、RR的间隙锁会导致锁定范围扩大。
2、条件列未使用到索引,RR锁表,RC锁行。
3、RC的“半一致性”(semi-consistent)读可以增加update操作的并发性。

在RC中,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)。

使用RC的好处:
https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html

MySQL锁汇总,一文搞懂InnoDB所有的锁_数据库_17


实际上,如果能够正确地使用锁(避免不使用索引去加锁),只锁定需要的数据,用默认的RR级别就可以了。


好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695