MySQL中的锁

锁是为了解决并发环境下资源竞争的伎俩,其中乐观并发管制,乐观并发管制和多版本并发管制是数据库并发管制次要采纳的技术手段(具体可见我之前的文章),而MySQL中的锁就是其中的乐观并发管制。

MySQL中的锁有很多品种,咱们能够依照上面形式来进行分类。

按读写

从数据库的读写的角度来分,数据库的锁能够分为分为以下几种:

  • 独占锁:又称排它锁、X锁、写锁。X锁不能和其余锁兼容,只有有事务对数据上加了任何锁,其余事务就不能对这些数据再搁置X了,同时某个事务搁置了X锁之后,其余事务就不能再加其余任何锁了,只有获取排他锁的事务是能够对数据进行读取和批改。
  • 共享锁:又称读锁、S锁。S锁与S锁兼容,能够同时搁置。
  • 更新锁:又称U锁。它容许再加S锁,但不容许其余事务再施加U锁或X锁,当被读取的数据要被更新时,则降级S锁为X锁。U锁的长处是容许事务A读取数据的同时不阻塞其它事务,并同时确保事务A自从上次读取数据后数据没有被更改,因而能够缩小X锁和S锁的抵触,同时防止应用S锁后再降级为X锁造成的死锁景象。留神,MySQL并不反对U锁,SQLServer才反对U锁。

兼容性矩阵如下(+ 代表兼容, -代表不兼容)

右侧是已加的锁XSU
X---
S-++
U-+-

按粒度

MySQL反对不同级别的锁,其锁定的数据的范畴也不同,也即咱们常说的锁的粒度。MySQL有三种锁级别:行级锁、页级锁、表级锁。不同的存储引擎反对不同的锁粒度,例如MyISAM和MEMORY存储引擎采纳的是表级锁,页级锁仅被BDB存储引擎反对,InnoDB存储引擎反对行级锁和表级锁,默认状况下是采纳行级锁。

特点

表级锁:开销小,加锁快;不会呈现死锁;锁定粒度大,产生锁抵触的概率最高,并发度最低。数据库引擎总是一次性同时获取所有须要的锁以及总是按雷同的程序获取表锁从而防止死锁。
行级锁:开销大,加锁慢;会呈现死锁;锁定粒度最小,产生锁抵触的概率最低,并发度也最高。行锁总是逐渐取得的,因而会呈现死锁。
页面锁:开销和加锁工夫界于表锁和行锁之间;会呈现死锁;锁定粒度界于表锁和行锁之间,并发度个别。

上面具体介绍行锁和表锁,页锁因为应用得较少就不介绍了。

行锁

按行对数据进行加锁。InnoDB行锁是通过给索引上的索引项加锁来实现的,Innodb肯定存在聚簇索引,行锁最终都会落到聚簇索引上,通过非聚簇索引查问的时候,先锁非聚簇索引,而后再锁聚簇索引。如果一个where语句外面既有聚簇索引,又有二级索引,则会先锁聚簇索引,再锁二级索引。因为是分步加锁的,因而可能会有死锁产生。

MySQL的行锁对S、X锁上做了一些更准确的细分,使得行锁的粒度更细小,能够缩小抵触,这就是被称为“precise mode”的兼容矩阵。(该矩阵没有呈现在官网文档上,是有人通过Mysql lock0lock.c:lock_rec_has_to_wait源代码揣测进去的。)

行锁兼容矩阵

  • 间隙锁(Gap Lock):只锁间隙,前开后开区间(a,b),对索引的间隙加锁,避免其余事务插入数据。
  • 记录锁(Record Lock):只锁记录,特定几行记录。
  • 临键锁(Next-Key Lock):同时锁住记录和间隙,前开后闭区间(a,b]。
  • 插入用意锁(Insert Intention Lock):插入时应用的锁。在代码中,插入用意锁,实际上是GAP锁上加了一个LOCK_INSERT_INTENTION的标记。
右侧是已加的锁(+ 代表兼容, -代表不兼容)GRNI
G++++
R++
N++
I++

S锁和S锁是齐全兼容的,因而在判断兼容性时不须要比照准确模式。准确模式的检测,用在S、X和X、X之间。从这个矩阵能够看到几个特点:

  • INSERT操作之间不会有抵触:你插入你的,我插入我的。
  • GAP,Next-Key会阻止Insert:插入的数据正好在区间内,不容许插入。
  • GAP和Record,Next-Key不会抵触
  • Record和Record、Next-Key之间互相抵触。
  • 已有的Insert锁不阻止任何筹备加的锁。
  • 间隙锁(无论是S还是X)只会阻塞insert操作。

留神点

  • 对于记录锁,列必须是惟一索引列或者主键列,查问语句必须为准确匹配,如“=”,否则记录锁会进化为临键锁。
  • 间隙锁和临键锁基于非惟一索引,在惟一索引列上不存在间隙锁和临键锁。

表锁与锁表的误区

只有正确通过索引条件检索数据(没有索引生效的状况),InnoDB才会应用行级锁,否则InnoDB对表中的所有记录加锁,也就是将锁住整个表。留神,这里说的是锁住整个表,然而Innodb并不是应用表锁来锁住表的,而是应用了上面介绍的Next-Key Lock来锁住整个表。网上很多的说法都是说用表锁,然而实际上并不是,咱们能够通过上面的例子来看看。

假如咱们有以下的数据(MySQL8):

mysql> select * from users;+----+------+-----+| id | name | age |+----+------+-----+|  1 | a    | 1   ||  2 | a    | 1   ||  3 | a    | 1   ||  4 | a    | 1   ||  5 | a    | 1   |+----+------+-----+

办法一:

咱们应用表锁锁表,并查看引擎的状态

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> lock tables users write;Query OK, 0 rows affected (0.00 sec)mysql>  show engine innodb status\G...------------TRANSACTIONS------------Trx id counter 4863Purge done for trx's n:o < 4862 undo n:o < 0 state: running but idleHistory list length 911LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 281479760456232, not startedmysql tables in use 1, locked 1   ###############留神这里0 lock struct(s), heap size 1136, 0 row lock(s)...

而后咱们再通过非索引的字段查问来加锁,并查看引擎的状态

## 先解锁上次的表锁mysql> unlock tables;Query OK, 0 rows affected (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> select * from users where name = 'a' for update;mysql>  show engine innodb status\G...------------TRANSACTIONS------------Trx id counter 4864Purge done for trx's n:o < 4862 undo n:o < 0 state: running but idleHistory list length 911LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 4863, ACTIVE 37 sec2 lock struct(s), heap size 1136, 6 row lock(s)    ###############留神这里...

而后咱们再删除id为2,3,4的数据,而后在通过非索引的字段查问来加锁,并查看引擎的状态

mysql> delete from users where id in (2,3,4);Query OK, 3 rows affected (0.00 sec)mysql> commit;Query OK, 0 rows affected (0.00 sec)mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> select * from users where name = 'a' for update;mysql>  show engine innodb status\G...------------TRANSACTIONS------------Trx id counter 4870Purge done for trx's n:o < 4869 undo n:o < 0 state: running but idleHistory list length 914LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 4869, ACTIVE 9 sec2 lock struct(s), heap size 1136, 3 row lock(s)   ###############留神这里...

能够看到这里应用了表锁和因为没法用索引锁定特定行而转而锁住整个表是不一样的。从第二次和第三次的操作来看,lock住的row也是不同的,这是因为两者间隙的个数不同,所以能够看到应用的并不是表锁,而是Next-Key Lock。第一次锁住了(-∞,1],(1,2],(2,3],(3,4],(4,5],(5,∞],第二次锁住了(-∞,1],(1,5],(5,∞]。

办法二:

也能够通过以下语句来查看锁的信息,也能够晓得用的是行锁,且是锁住了区间(插入不了数据)和记录,所以是Next-Key Lock。

mysql> select ENGINE_TRANSACTION_ID,LOCK_TYPE,LOCK_MODE from performance_schema.data_locks where ENGINE_TRANSACTION_ID in (你的事务id);+-----------------------+-----------+-----------+| ENGINE_TRANSACTION_ID | LOCK_TYPE | LOCK_MODE |+-----------------------+-----------+-----------+|                  4889 | TABLE     | IX        ||                  4889 | RECORD    | X         ||                  4889 | RECORD    | X         ||                  4889 | RECORD    | X         |+-----------------------+-----------+-----------+10 rows in set (0.00 sec)

LOCK_TYPE:对于InnoDB,可选值为 RECORD(行锁), TABLE(表锁)

LOCK_MODE:对于InnoDB,可选值为S[,GAP], X[,GAP], IS[,GAP],IX[,GAP], AUTO_INC和UNKNOWN。除了AUTO_INC和UNKNOWN,其余锁定模式都蕴含了GAP锁(如果存在)。

具体可见 MySQL文档:https://dev.mysql.com/doc/ref...

表级锁

间接对整个表加锁,影响表中所有记录,表读锁和表写锁的兼容性见下面的剖析。

MySQL中除了表读锁和表写锁之外,还存在一种非凡的表锁:意向锁,这是为了解决不同粒度的锁的兼容性判断而存在的。

意向锁

因为锁的粒度不同,表锁的范畴笼罩了行锁的范畴,所以表锁和行锁会产生抵触,例如事务A对表中某一行数据加了行锁,而后事务B想加表锁,失常来说是应该要抵触的。如果只有行锁的话,要判断是否抵触就得遍历每一行数据了,这样的效率切实不高,因而咱们就有了意向表锁。

意向锁的次要目标是为了使得 行锁表锁 共存,事务在申请行锁前,必须先申请表的意向锁,胜利后再申请行锁。留神:申请意向锁的动作是数据库实现的,不须要开发者来申请。

意向锁是表级锁,然而却示意事务正在读或写某一行记录,而不是整个表, 所以意向锁之间不会产生抵触,真正的抵触在加行锁时查看。

意向锁分为动向读锁(IS)和动向写锁(IX)。

表锁的兼容性矩阵

右侧是已加的锁(+ 代表兼容, -代表不兼容)ISIXSX
IS+++
IX++
S++
X

参考资料

https://www.cnblogs.com/rjzhe...

https://dev.mysql.com/doc/ref...

版权申明

转载请注明作者和文章出处
作者: X学生
https://segmentfault.com/a/1190000023869573