共计 4966 个字符,预计需要花费 13 分钟才能阅读完成。
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 锁。
兼容性矩阵如下(+ 代表兼容,- 代表不兼容)
右侧是已加的锁 | X | S | U |
---|---|---|---|
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 的标记。
右侧是已加的锁(+ 代表兼容,- 代表不兼容) | G | R | N | I |
---|---|---|---|---|
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 4863
Purge done for trx's n:o < 4862 undo n:o < 0 state: running but idle
History list length 911
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 281479760456232, not started
mysql 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 4864
Purge done for trx's n:o < 4862 undo n:o < 0 state: running but idle
History list length 911
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 4863, ACTIVE 37 sec
2 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 4870
Purge done for trx's n:o < 4869 undo n:o < 0 state: running but idle
History list length 914
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 4869, ACTIVE 9 sec
2 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)。
表锁的兼容性矩阵
右侧是已加的锁(+ 代表兼容,- 代表不兼容) | IS | IX | S | X |
---|---|---|---|---|
IS | + | + | + | – |
IX | + | + | – | – |
S | + | – | + | – |
X | – | – | – | – |
参考资料
https://www.cnblogs.com/rjzhe…
https://dev.mysql.com/doc/ref…
版权申明
转载请注明作者和文章出处
作者: X 学生
https://segmentfault.com/a/1190000023869573