目录
mysql的锁
概述:根据mysql锁的大类型可以分为
我们先来讲一下范围最大的全局锁
使用
为什么要使用全局锁?
使用全局锁进行备份的缺点
表级锁
表锁
1.共享读表锁的语法
2.排斥写表锁
元数据锁
意向锁
什么是意向锁
怎么产生意向锁
意向锁的作用
AUTO-INC 锁
行级锁
记录锁
记录共享锁
记录排斥锁
间隙锁
next-key锁
编辑
插入意向锁
行级锁是怎么加锁的
主键索引等值查询
1.等值查询的值存在,next-key锁退化成这个值的记录锁。
2.等值查询的数据不存在,next-key锁会退化成间隙锁
编辑 主键索引范围查询
大于的情况
1.条件值不存在 如 id>5 5是条件值
2.条件值存在
小于的情况
1.条件值不存在
2.条件值存在
编辑
二级索引的普通索引的等值查询
1.等值查询的值存在
2.查询的值不存在
普通索引的范围查询
大于的情况
1.条件值不存在
2.条件值存在
小于的情况
1.条件值存在
编辑 2.条件值不存在
没有使用的索引字段进行查询,修改删除的加锁情况
死锁的产生
死锁就是这样产生的
间隙锁在不同的事务之间可以共存,那不是会频繁的导致死锁的产生吗?
死锁会大概率发生在最后一个数据之后的那一段区间
那如果发生了死锁要怎么去解决
插入遇见唯一键冲突
mysql的锁
主要规则:读读锁兼容,写写锁不兼容,读写锁不兼容
概述:根据mysql锁的大类型可以分为
- 全局锁
- 表级锁:表锁 意向锁 元数据锁 AUTO-INC锁
- 行级锁:记录锁,间隙锁,next-key锁(记录锁加间隙锁)
我们先来讲一下范围最大的全局锁
使用
只要全局锁一旦加上, 所有的增删改的操作都不能被执行,整个数据库只会处于只读状态
在一个会话里加上全局锁
flush tables with read lock
只能执行读操作,执行增删改操作会直接报错
在其他会话里
也可以执行查询操作,但是执行增删改操作会被阻塞,直到全局锁释放才会去执行当前操作
解锁
unlock tables
解锁之后 ,阻塞的操作就会被执行
为什么要使用全局锁?
我们需要对整个数据库的数据进行备份时 ,就需要使用全局锁。
如果在一个库中有一张订单表,商品余额表
当我们对这个数据库的数据进行备份时,先对订单表进行备份,这时一个其他的并发事务执行了购买操作,减少的订单余额,这时候再去备份的商品余额表就与之前备份的订单表数据不一致了。
使用全局锁进行备份的缺点
当我们使用全局锁时,只有查询的业务可以运行,而增删改的业务都被阻塞,这是我们十分不想看到的,那有什么方法可以解决吗,有的,如果数据库的引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建 Read View,,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。
这就是事务中的隔离性,就算其他事务对数据表进行了操作,数据库备份时只会对read view进行备份
备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 –single-transaction
参数的时候,在备份数据库之前先开启事务。这种操作只适合于可重复读级别的隔离机制,而mysql默认的innodb存储引擎就是可重复读的隔离机制。
表级锁
表锁
我们现在来讲以下表级锁的表锁 ,这是一个用来锁着一整张表的锁。
可以分为共享读表锁,排斥写表锁。
1.共享读表锁的语法
共享读表锁就是让这张表只能读,增删改操作都不可以执行
lock tables 表名 read;
演示共享读表锁
在一个会话里给person表加上共享读表锁
可以发现对这张表只能读,增删改操作都会直接报错
在其它会话里
也可以读,但是增删改操作都会被阻塞
释放锁
其他会话的阻塞操作也会被执行
注意:如果当前会话对person表加了共享读表锁后,也不能操作其他表的数据了,连查询都不可以,其他会话则可以
2.排斥写表锁
如果当前会话对一张表加了排斥写表锁,那么只能让当前对话能对这张表进行CRUD操作,其他会话不能查询这个表,也不能增删改
lock table 表名 write;
演示
当前会话对person加了排斥写表锁,可以进行CRUD操作
其他会话,所有对这个表的操作都会被阻塞
我们也可以发现对整个表直接进行加排斥写锁后,其他会话都无法操作 这个表,对并发事务十分不友好,性能十分低下,所以innodb有了行锁这种更小粒度的锁。
元数据锁
我们之前讲的全局锁和表锁都是需要我们进行显示调用的,而这个元数据锁,意向锁(增删改)和行锁(增删改)是自动调用的,所以我们需要开启事务来查询加锁的情况,因为事务一旦结束,这些锁都会自动释放。这也是我们为什么一定要开启事务来查看加锁的情况,因为mysql是自动提交事务的(执行一行sql后自动提交),然后这些自动加的锁我们就无法观察到了。
元数据锁的作用是什么-->用来解决DML语句和DDL语句的冲突,假设我们在事务a中进行了增删改操作,而事务b把表的结构给改了,这显然是不被允许的。
元数据锁的类型-->MDL读锁(进行CRUD时的事务进行加的锁)
-->MDL写锁(进行修改表结构时的事务加的锁)
这两个锁是不兼容的,在事务a中先对person表进行了CRUD操作,那么这个表会被加上MDL读锁,这时事务b想对person表进行DDL,操作表数据时,想要去给这个表加MDL写锁时,因为MDL读锁与MDL写锁不兼容,所以MDL写锁无法加上,DDL语句也无法执行,b事务就只能阻塞等待a事务提交释放MDL读锁。
但是经过我的操作,如果先在事务a修改的表结构,在事务b中可以进行CURD操作
演示
在事务a中进行CURD操作,这是这张表已经被加上的MDL读锁
事务b想要去修改表结构时,会阻塞,因为MDL写锁无法加上,与MDL读锁无法兼容,会阻塞等待
事务a提交
表结构成功改变
意向锁
意向锁进行增删改操作也是自动调用加锁的,事务结束自动解锁。
什么是意向锁
意向锁和行锁是息息相关的,我们在对行数据进行加锁时,会先产生一个表级别的意向锁,共享读行锁产生意向共享锁,排斥写行锁产生意向排斥锁。
意向锁之间也不会发生冲突
- 意向共享锁读可以兼容共享读表锁,与排斥写表锁不兼容(读读锁共享)
- 意向排斥写锁与共享读表锁不兼容,与排斥写表锁不兼容(写读锁不共享,写写锁不共享)
怎么产生意向锁
普通的select语句不会产生行锁,只会产生MDL读锁
这就要先讲一下怎么产生共享读行锁,排斥写行锁
共享读行锁:加上这个共享读行锁之前,先在表加上了意向共享读锁
select ... lock in share mode;
排斥写行锁:加上这个排斥写行锁之前,先在表加上了意向排斥写锁
select ... for update;
当执行插入、更新、删除操作,需要先对表加上「意向独占锁」 ,然后对该记录加独占锁。
执行插入操作
产生了意向排斥锁
IX
select * from performance_schema.data_locks\G;
意向锁的作用
在对一张表加表锁时,需要判断这张表里有没有行锁,如果没有意向锁,那就需要去全表扫描判断有没有行锁,这显然效率十分低下,如果有了意向锁就可以直接判断这张表里有没有行锁。
AUTO-INC 锁
现在数据表进行插入数据时,主键都是自增的,使用auto_increment;
AUTO-INC锁就是实现这个的基础,在插入数据时,先给这个表加上AUTO-INC锁,然后把赋值数据给自增字段,插入完成之后,锁会自动释放
所以这个AUTO-INC锁不是事务结束后释放,而是执行插入语句结束后释放。
AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞(等着被赋值)。
InnoDB 存储引擎提供了一种轻量级的锁来实现自增。
一样也是在插入数据的时候,会为被 AUTO_INCREMENT
修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。
行级锁
innodb相比于myisam的三大特点:支持事务,外键约束,行级锁
行级锁的类型主要有三类:
- Record Lock,记录锁,也就是仅仅把一条记录锁上(更新和删除数据时会对这个数据加排斥写锁,插入数据时也会,防止被其他事务修改);
- Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
- Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。(加入有1,3两个数据,next-lock锁的是3记录本身,和1,3之间的范围)
记录锁
记录锁也分有记录共享读锁和记录排斥写锁,也是经典的读读锁兼容,读写锁不兼容,写写锁不兼容
记录共享锁
如何加上记录共享读锁
再使用select 语句后加上lock in share mode;
演示:
这里给id=1的数据加上了记录共享读锁
可以看到现在的锁有表级锁 意向共享锁,记录共享锁
S,REC_NOT_GAP就是 记录共享锁,然后lock_data加锁的行数据
其他事务也可以继续加记录共享锁
现在就有四个锁了 ,也是可以发现意向锁之间可以兼容,而是与共享读表锁和排斥写表锁不兼容
但是不可以加记录排斥锁
加了就阻塞
记录排斥锁
使用更新,删除操作时,还有在select 语句后加for update时就加上记录排斥锁
给id=1的数据加上记录排斥锁
可以发现现有两个锁,意向排他锁,
其他事务要更新这个id=1的数据时会阻塞,因为更新和删除操作都要去加排斥记录锁,但是现在事务a已经给id=1加了排斥记录锁,而排斥记录锁之间不兼容,所以会阻塞。
在事务a中给id=2的数据加上排斥记录锁
就会有两个意向排斥锁,也可以看出来意向锁之间互相兼容
间隙锁
间隙锁存在于可重复读的隔离机制中,主要用来解决幻读的问题,什么是幻读-->就是在一个事务中前后查询的记录数不同(其他事务进行了删除操作或者插入操作)
间隙锁虽然也分为S型和X型,但是不同的间隙锁之间是兼容的。
演示:
现在查询了id=3的数据,这是不存在的,所以会在(2,4)之间加了一个间隙锁。
这就会有两个锁
一个锁是表级别的意向排斥锁,还有一个是行级的间隙锁
X,GAP 4 就是id=4数据前面那段的区间,也就是(2,4)
插入id=3的数据时就会阻塞,因为(2,4)这个区间已经加了间隙锁 ,想要插入数据时,就需要获取一个区间的插入意向锁(行级锁),这个锁与间隙锁不兼容,所以会阻塞,无法获取插入意向锁
next-key锁
next-key是间隙锁和记录锁的总和,其实间隙锁和记录锁都是由next-key临键锁退化来的(怎么退化,下文会讲)
注意next-key也是满足读读锁兼容,读写锁不兼容,写写锁不兼容。
虽然间隙锁之间都互相兼容,但是记录锁不是互相兼容的。
演示
在事务a查询id>=12的数据加next-key锁
事务b有一个表级别的意向排斥锁
还有12数据的记录锁,还有(12,+∞】的next-key锁 因为这里∞的数据是不存在的,所以只是不能插入id大于12的数据
如果加的是(2,4]的next-key锁,就不能插入id=3的数据,也不能修改或者删除4的数据
插入意向锁
插入意向锁是一个行级锁,是想要在一个区间内插入数据时要获取的锁,与间隙锁不兼容
演示:
无法插入id=15的数据,因为在(12,+无穷]的区间里有间隙锁,所以无法加上这个区间内的插入意向锁
也就是说加入一个事务a有了一个区间的间隙锁,其他事务就无法获取这个区间的插入意向锁,无法插入数据,但是事务a是可以插入数据的,因为这个锁是事务a的 (锁就是防止并发事务的操作导致数据不一致)
行级锁是怎么加锁的
我们上文只是简单介绍了记录锁,间隙锁和next-key锁的简单概念
这里我们来介绍一下行级锁是怎么加的
主键索引等值查询
1.等值查询的值存在,next-key锁退化成这个值的记录锁。
这个记录就不会被其他事务进行修改和删除
可以发现对id=1的数据加了记录锁
2.等值查询的数据不存在,next-key锁会退化成间隙锁
id=3的数据不存在,会在(3,4)范围加上范围锁
主键索引范围查询
大于的情况
1.条件值不存在 如 id>5 5是条件值
条件值不存在,>和>=的情况是一样的
这里我查询id>=11的数据
11的数据不存在,会在(10,12]加上next-key锁,还有(12,+无穷]的next-key锁
2.条件值存在
>11的情况
id=11存在,会在 (11,12]加上next-key锁和(12,+无穷]加上next-ley锁
>=11的情况
id=11存在,会在id=11的记录锁,(11,12]加上next-key锁和(12,+无穷]加上next-ley锁
小于的情况
1.条件值不存在
<2的情况 id=2数据不存在
在(1,3)加上间隙锁,(-无穷,1]加上next-key锁
<=2的情况
同上
2.条件值存在
<2的情况
(1,2)间隙锁,(-无穷,1]加上next-key锁
<=2的情况
(1,2]的next-key锁, (-无穷,1]加上next-key锁
二级索引的普通索引的等值查询
因为唯一索引的加锁情况跟主键索引的情况类似,只不过给唯一索引加锁时,还要给主键索引的记录加上记录锁,因为二级索引的数据就是主键id
条件值存在:因为普通索引的值不是唯一的,所以普通索引的等值查询也是一个扫描的过程,在扫描的过程中,会给符合条件的普通索引加上next-key锁,并给对应的主键id的索引加上记录锁。直到第一个不符合条件的索引key值,然后这个不符合条件的索引的next-key退化成间隙锁
条件值不存在:就是扫描到第一个比条件值大的索引时,这个索引的next-key直接退化成间隙锁
这里给age字段加上普通索引
1.等值查询的值存在
如查询 age=17
会在(16,17]加上next-key锁,(17,18)就加间隙锁,这里假设16,18的值存在
还会给age=17的记录的主键索引加上记录锁
查询age等于25
可以发现加的锁有(24,25]id=1的next-key锁,(25,25]id=7的next-key锁,(25,27)的间隙锁,还有在主键索引的id=1和id=7的记录锁
这里注意,如果插入age=24的数据,且id<6(当前age=24的数据id=6)可以插入,因为,没有间隙锁,如果插入age=24,id>6就无法插入,阻塞等待,因为有24,25]id=1的next-key锁
注:二级索引的b加树上,字段值相同就按主键值排序
还有如果插入age=27的数据,且id<8则无法插入((25,27)的间隙锁),age=27,id>8可以插入
2.查询的值不存在
会加一个间隙锁
普通索引的范围查询
普通索引进行范围查询时,不会出现next-key锁退化成记录锁和间隙锁的情况
大于的情况
1.条件值不存在
如age>=26
会在(25,27]的next-key锁,后面的数据都是next-key锁
2.条件值存在
如age>27
在(27,28]加上next-key锁,(28,+无穷]加上next-key锁,还有28的主键id也加上记录锁
如age>=27
会在(25,27]id=4加上next-key锁,后面也都是next-key锁
这里28虽然是最后一条数据,但是mysql会维护一个supremum pseudo-record的值,代表无穷大
这是一个(28,+∞]的next-key锁
还会在主键索引的id=4,8,9,10的索引上加记录锁,防止被其他事务进行修改和删除
为什么第一个27的索引数据不会跟主键索引一样退换成记录锁,因为一旦退化成记录锁,那么其他事务是可以在第一个27索引数据前 插入一个age=27,且id<4的新数据,那么这样就会导致数据前后不一致。
小于的情况
1.条件值存在
可以发现这里加了(-无穷,13]id=2,(13,13]id=3的next-key锁,也给id=1,3主键索引加了记录锁,这都是正常的,但是(13,18]的next-key锁并没有退化成间隙锁,也没有给id=18的主键索引加上记录锁
其他事务想要去修改age=18的数据也会被阻塞
使用主键id索引就可以修改成功
所以我还是不太清楚为什么(13,18]的next-key锁不直接退化成间隙锁,这样都无法直接修改age=18的数据
就算是<= ,后面第一个不符合数据的索引的数据也不会退化成记录锁
(13,18]next-key锁不会退化成间隙锁
2.条件值不存在
(13,18]这个next-key锁也不会退化成间隙锁 ,但是并没有对主键索引id=12加上记录锁
没有使用的索引字段进行查询,修改删除的加锁情况
如果没有使用索引字段进行操作,就会去把全表扫描把所有主键索引id的记录都加上next-key的行级别锁,导致其他事务无法对这张表进行操作,这显然是不对。
name字段没有索引
给每一条主键索引都加上了next-key锁,就相当于加上了表级别的排斥写表锁
死锁的产生
这里我们首先得先知道
意向锁也可以共存,不同事务都可以对同一张表进行加意向锁,因为意向锁的存在是防止其他事务直接对这张表直接加上表级锁(意向共享锁可以于共享读表锁兼容,不可以于排斥写表锁兼容)
间隙锁的意义只在于阻止区间被插入,因此不同事务之间加的间隙锁是可以共存的 ,共享和排他的间隙锁是没有区别的,他们相互不冲突,且功能相同,即两个事务可以同时持有包含共同间隙的间隙锁。
这里的共同间隙包括两种场景:
- 其一是两个间隙锁的间隙区间完全一样;
- 其二是一个间隙锁包含的间隙区间是另一个间隙锁包含间隙区间的子集。
而接着我们要了解插入意向锁,这个锁是想要在某一个区间进行插入数据时要加的锁,而且事务a在一个区间有了间隙锁,事务b无法在这个区间获取插入意向锁,也就无法插入数据。可以发现同一个区间的插入意向锁和间隙锁在不同的事务之间不兼容。
死锁就是这样产生的
事务a和事务b都在某一个区间有着相同的间隙锁,那事务a想要在这个区间插入数据时,因为事务b有着这个区间间隙锁,所以事务a就无法操作,而事务b也想要插入数据时,也无法插入,所以这两个事务就一直相互阻塞着,导致死锁。
间隙锁在不同的事务之间可以共存,那不是会频繁的导致死锁的产生吗?
所以虽然间隙锁可以共存,但是记录锁不可以,而next-key是包括间隙锁和记录锁的所以只要某一个区间内包括记录锁,不同的事务之间就无法加上相同范围的锁,就可以大概率避免死锁的发生。
死锁会大概率发生在最后一个数据之后的那一段区间
最后一个数据之后加的next-key锁是 (最后一个数据,+无穷】,但是无穷这个值是不存在的,也可以近似的看作成是间隙锁。
那如果发生了死锁要怎么去解决
死锁的条件:
互斥、占有且等待、不可强占用、循环等待
1.设置事务等待锁的超时时间
当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了,在 InnoDB 中, 参数 innodb_lock_wait_timeout
是用来设置超时时间的,默认值时 50 秒。
2.开启主动死锁检测
主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect
设置为 on,表示开启这个逻辑,默认就开启。
所以当事务a,b都有最后一段的间隙锁时,又都想对这个区间进行插入数据,相互阻塞导致死锁时,这两个事务会有一个事务回滚退出事务,假设b事务退出,那么事务b加的锁也就会释放,那么事务a就可以在这个区间插入数据了。
插入遇见唯一键冲突
插入意向锁是针对某一个区间的。
那如果我往已经存在的主键id又插入一个id相同的数据,会直接报错误,插入新记录的事务还会给已存在的主键值重复的聚簇索引记录添加 S 型记录锁。
如果唯一二级索引重复,插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录添加 S 型 next-key 锁。
插入普通索引的值重复,因为普通索引的next-key不会直接退化成记录锁,如 age=18 for update;
会在(17,18]有next-key锁,(18,19)有间隙锁,所以不会导致插入相同数据的值,导致幻读。