标题: 强人锁男,MySQL到底有多少锁?
原文地址: https://blog.csdn.net/Baisitao_/article/details/104829887
作者: Sicimike
本文是我点评其他博主的优秀文章,加了一些自我看法,希望对大家有帮助。
前言
读锁写锁意向锁,表锁行锁页面锁。
在学习Java并发编程的时候,肯定少不了学习 锁
。最常见的就是synchronized
,锁的概念不是很好理解,有的地方说是锁住了一段代码,有的地方说是锁住了一个对象。弄得初学者都是丈二和尚——摸不着头脑。
抛开这些结论性的说法,说一下我对锁的理解(不管是Java中的锁还是数据库中的锁,还是分布式锁)。当我们需要限制某段程序在同一时刻,最多能被1个线程同时执行的时候就需要锁。这个幸运的线程怎么选出来呢?那就让他们去抢一个许可证 吧,重点是需要保证这个许可证是唯一的,一次最多只能被一个线程抢到。 许可证 不要拘泥于是this
,还有可能是数据库设置了唯一键的列 、缓存中唯一的key或者一个文件目录。只有抢到了这个 许可证 的线程,才可以执行这段代码,执行完成或者异常退出后自动释放许可证 。
理解了这点,再说锁住的是一段代码、一个对象,甚至是一个线程都无所谓了,因为锁的作用就是在某一段时间内将一段代码、一个对象(许可证)、一个线程绑定在一起。
MySQL中的锁与存储引擎有关,MyISAM
只支持 表级锁 ,InnoDB既支持 表级锁 ,又支持 行级锁 。
InnoDB中的锁
InnoDB实现了 行级锁 ,可以分为两种类型: 共享 (S)锁定和 排他 (X)锁定。
- 共享(S)锁允许持有该锁的事务读取一行,所有又叫 读锁
- 排他(X)锁允许持有该锁的事务更新或删除行,所以又叫 写锁 。
多个事务并发执行时,如果事务T1在某一行r
上持有 共享(S)锁 ,那么事务T2的对这行r
的锁请求将按以下方式处理:
- T2对 S锁 的请求可以立即获得批准。T1和T2都在
r
上保持了 S锁 。 - T2对 X锁 的请求不能获取批准。
如果事务T1在某一行r
上拥有 排他(X)锁 ,则事务T2不能获取锁(不论是 S锁 还是 X锁 )
换言之,S锁和S锁是兼容的,S锁和X锁是冲突的,X锁和X锁是冲突的
S锁 | X锁 |
---|---|
S锁 | 冲突 |
X锁 | 冲突 |
但是 共享锁 和 排它锁 并不是指具体的两种锁,而是指 两类 锁。
同样的 乐观锁 和 悲观锁
也不是指具体的锁,而是指两类锁。乐观锁是乐观的认为每次都不会发生冲突,只会在更新的时候检查要更新的值有没有被别人修改过,因为没有加锁,所以乐观锁又叫
无锁 。在数据库中一般是用MVCC实现乐观锁,在Java中用CAS实现乐观锁。至于悲观锁就是悲观的认为每次都会发生冲突,所以每次修改都需要加锁。
表锁
表锁 是MySQL中粒度最大的一种锁,简单粗暴的锁住整张表,实现简单所以支持的并发度低。InnoDB和MyIASM都支持表锁,表锁分为
共享(S)锁 和 排他(X)锁 。
表锁 的特点是实现简单,并发度低。加锁快,开销小。不会出现死锁。
行锁
行锁 是MySQL中粒度最细的一种锁,每次只锁住要操作的那一行。实现复杂,支持的并发度高。只有InnoDB支持行锁。行锁也分为 共享(S)锁
和 排他(X)锁 。行锁是对 索引 的锁定。例如SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
,防止任何其他事务插入,更新或删除t.c1
值为10的行。行锁始终锁定 索引 ,对于没有创建索引的表,InnoDB创建一个
隐藏的聚簇索引 并将该索引用于行锁。
行锁 的特点是实现复杂,并发度高。加锁慢,开销大。会出现死锁。
意向锁(Intention Locks)
InnoDB支持多种粒度锁定,允许 行锁 和 表锁 并存。为了使在多个粒度级别上的锁定变得切实可行,InnoDB实现了 意图锁 。
意向锁是表级锁 ,表示事务稍后对表中的行需要上哪种类型的锁( 共享锁 或 排他锁 )。有两种类型的意图锁:
- 意向共享锁 (IS)表示事务打算给数据行加行 共享锁 ,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
- 意向排他锁 (IX)表示事务打算给数据行加行 排他锁 ,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
SELECT ... LOCK IN SHARE MODE
设置 IS锁定 ,而SELECT ... FOR UPDATE
设置
IX锁定 。
表级锁 之间的兼容性如下
X锁 | IX锁 | S锁 | IS锁 |
---|---|---|---|
X锁 | 冲突 | 冲突 | 冲突 |
IX锁 | 冲突 | 兼容 | 冲突 |
S锁 | 冲突 | 冲突 | 兼容 |
IS锁 | 冲突 | 兼容 | 兼容 |
有的同学一看到这么多种情况就头晕,死记硬背是不可能死记硬背的,这辈子都不会死记硬背。既然这样,那就干脆不记,花点时间深入了解一下为什么要设计成表中这样。
要想理解这个表,首先得理解为什么要有意向锁(Intention Locks),关于意向锁的作用,官方文档上给出了这么一句话:
Intention locks do not block anything except full table requests (for
example, LOCK TABLES … WRITE). The main purpose of intention locks is to
show that someone is locking a row, or going to lock a row in the table.
张三思评:
意向锁只和表锁互斥! 意向锁本身就是表级锁, 它的目的在于,告诉其他用户, 在不久的将来(或者正在,这一点别忘了),有一个事务希望锁住某一行.
意向锁 不会阻止任何其他请求,除了(锁定)全表请求(例如LOCK TABLES ... WRITE
)外。 意向锁
定的主要目的是:声明已经有事务 正在锁定 表中的行,或者 即将锁定 表中的行。
这句话透露出了意向锁虽然是表锁,但是和行锁是完全兼容的(包括 共享锁 和 排它锁 )。但是声明表中的行正在被锁定或者即将被锁定,有什么用呢?
假设事务A给表中某一行加了排它锁,而事务B想给这个表加一个全表的排他锁,这时候事务B就需要判断当前表中到底有没有排他锁,如果有的话,是不能成功加上去表的排它锁的。此时,如果没有意向锁,事务B只能遍历整个索引去判断,这样无疑是低效的。为了解决这个问题,MySQL引入了意向锁。当事务A给某一行加了排它锁或者共享锁后,会分别在表上加
意向排它锁 (IX)或者 意向共享锁 (IS),这时,事务B就可很轻松的判断当前表是否有 行锁
,这也就是前文所说的:为了使在多个粒度级别上的锁定变得切实可行,InnoDB实现了 意图锁 。
张三思评:
说白了,其中一个场景就是为了给后面加表锁省事些.
理解了意图锁的作用,再来看看上面的表格。S锁和X锁之间的兼容性前文已经理清了,还剩下IX和IS之间以及S/X和IS/IX的兼容性。
由于某一行被加了S锁或者X锁后,表上都会加上对应的IS锁和IX锁。另一个事务想锁住另一条记录,也得加上对应的IS锁或者IX锁,所以IS和IS、IS和IX以及IX和IX必定是兼容的,不然整个表最多只能上一个行锁。
至于S锁、X锁和IS锁、IX锁的兼容性则需与分情况讨论了。当整个表上了X锁之后,再也不能上别的X锁或者S锁了,所以X锁和IS、IX都是冲突的。当整个表上了S锁之后,不能再上X锁了,但是还可以上S锁,所以S锁和IX锁是冲突的,但是和IS锁是兼容的。
这样理解了之后,再看上面的那个表格,似乎也变得有规律了。
间隙锁(Gap Locks)
间隙锁锁定的是索引记录之间的间隙,或者在第一个之前或最后一个索引记录之后的间隙。例如SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE
,可以防止其他事务将t.c1
为15的记录插入表中,因为这两个值之间的间隙是被锁定的。
间隙可能跨越单个索引值,多个索引值,甚至为空。
间隙锁的唯一目的是防止其他事务在间隙中插入数据。间隙锁可以 共存 。一个事务执行的间隙锁,不会阻止另一事务对相同的间隙进行间隙锁定。
张三思评:
防止幻读, 造成区间查询不准确. 但是这也明显降低了并发读. 那么问题来了, 什么场景下会间隙锁呢? 除了SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE
这种语句,还有其他的场景吗?
如何模拟出间隙锁呢? 求大佬指点.
间隙锁的子类, 插入意图锁(Insert Intention Locks)
插入意图锁是在行插入之前,通过INSERT
操作设置的 间隙锁 的一种类型。如果多个事务想要在 同一个间隙
中插入不同的值(也就是插入的位置不同),则这多个事务均不会被阻塞。假设有索引记录,其值分别为4和7。单独的事务分别尝试插入值5和6,在获得插入行的排他锁之前,每个事务都使用插入意图锁来锁定4和7之间的间隙,但不会互相阻塞,因为插入的行是无冲突的。
Next-Key Locks
Next-Key Locks是索引记录上的 行锁 和 索引记录之前的间隙上的间隙锁 的组合。如果一个session在索引中的记录R上具有 共享 或 排他锁 ,则另一session不能按照索引顺序在R之前的间隙中插入新的索引记录。
默认情况下,InnoDB设置的事务隔离级别是REPEATABLE READ
。在这种情况下,InnoDB使用 Next-Key Locks
进行搜索和索引扫描,这可以防止幻读(虚读)。关于MySQL隔离级别相关的问题,请参考:面试官:MySQL事务是怎么实现的
自增锁(AUTO-INC Locks)
AUTO-INC锁是一种特殊的 表级锁,由事务插入具有AUTO_INCREMENT
列的表中获得。在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务都必须等待这个事务在该表中进行插入,以便第一个事务插入的行接收连续的主键值。
MyISAM中的锁
相比之下MyISAM中的锁就简单多了,因为MyISAM只支持表锁。并且 共享锁 和 排它锁 也满足如下关系
S锁 | X锁 |
---|---|
S锁 | 冲突 |
X锁 | 冲突 |
死锁
前文提到InnoDB行锁可能是出现 死锁
,死锁是一个计算机领域的概念,而不是数据库特有的,所以死锁的概念是通用的。不太了解死锁的同学请参考:面试官:请手写一段必然死锁的代码。这里演示下MySQL行锁导致的死锁,这也是官网上给出的例子
首先准备数据
## 创建表
CREATE TABLE t (i INT) ENGINE = InnoDB;
## 新增数据
INSERT INTO t (i) VALUES(1);
具体操作的时间线如下
事务A | 事务B |
---|---|
T1 | START TRANSACTION |
T2 | SELECT * FROM t WHERE i = 1 LOCK IN SHARE MODE |
T3 | |
T4 | |
T5 | DELETE FROM t WHERE i = 1 |
张三思评:
T2 事务A加了行S锁
T4 事务B加了行X锁,陷入阻塞,等待行上的S锁释放.
T5 事务A又想加X锁,但是这时候锁无法升级?
如果我们这么想, 事务A持有S锁, 那么应该可以直接升级成X锁,这样一来,就不会有死锁的现象了. 然而 MySQL 好像是讲究公平锁的, 好像是要排队上X锁啊! 这样就会造成死锁了.
事务A在T5执行时,事务B会收到一条错误信息
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
张三思评:
我测试的时候,并没有提示Deadlock found when trying to get lock;
,实际上在使用命令 commit 结束事务的之前,事务B一直是死锁的! 然而,我又尝试了一次,这一次成功了,就如作者描述的一样, 多试了几次, 基本和作者所述一致.
此处发生死锁,因为事务A需要X锁才能删除该行。但是,不能授予该锁定请求,因为事务B已经具有X锁定请求,并且正在等待事务A释放其S锁定。由于B事先要求X锁,因此A持有的S锁也不能升级为X锁。结果,InnoDB为其中一个客户端生成一个错误并释放其锁。此时时,可以授予对另一个客户端的锁定请求,并从表中删除该行。
也就是说MySQL可以自动检测死锁,并且放弃一个事务来成全另一个事务,这点与Java程序中的死锁不一样。
查询事务、锁相关的参考命令如下:
## 查看当前事务状态
select trx_id, trx_state, trx_started, trx_requested_lock_id, trx_wait_started, trx_query, trx_isolation_level from information_schema.innodb_trx;
## 查看当前锁定的事务
select * from information_schema.innodb_locks;
## 查看当前正在等待锁的事务
select * from information_schema.innodb_lock_waits;
总结
锁是MySQL非常重要的一个部分,虽然一般情况下锁的锁定和释放都由MySQL自动完成。但是了解MySQL中的锁还是很有必要,它让我们进一步的了解了MySQL是如何处理并发的。