深入理解MySQL锁
一、简单介绍一下锁结构吧
谈到锁,我们都知道锁的作用,但是我们先来简单讲讲锁的结构吧🤣🤣🤣
其实锁就是内存中的一个结构,在我们执行一个事务之前,MySQL表中的记录本来是没有锁的,也就是说一开始是没有锁结构和这条锁记录相关联的,如图所示:
如果事务T1想要对这条记录做改动时,它先到内存中查看有没有与这条记录相关联的锁,如果没有这个锁,就会在内存中生成一个锁结构与这条记录关联,如图:
图中表示生成了一个锁结构,但是其中的两个属性是啥意思呢?
trx
信息:代表这个锁结构是哪个事务生成的。is_waiting
:代表当前事务是否在等待。
由于事务 T1 在改动该记录的时候没有其他的锁结构与这条记录相关联,所以事务 T1 生成的这个锁结构的 is_waiting
属性是 false, 我们就可以把这个场景叫做加锁成功。假如此时有另外的一个事务 T2 来改动这个记录,发现有一个锁记录与之关联,此时事务T2 也会生成一个锁结构与之关联, 但是此时 T2 生成的锁结构的is_waiting
属性时 true,因为 T1 此时在改动这条记录,事务 T2 就需要等待,我们把这个场景叫做加锁失败,如图所示:
在事务 T1 把事务提交之后,就会释放它的锁结构,然后去内存中查看有没有其他事务在等待,此时发现事务 T2 正在等待这个锁,就是把事务 T2 对应锁结构的 is_waiting 属性修改为 false,然后唤醒事务 T2 的线程 ,这样事务 T2 就获取到锁了,可以对该记录进行改动了,如图:
好了,以上讲解了最基本的锁结构、锁结构与事务之间的关联以及事务之间获取锁的调度,下面我们就正式开始讲解各种锁吧😅😅😅
二、各种各样的锁
在正式讲锁之前,我们先来看看什么是一致性读吧。
事务利用 MVCC 进行的读取就叫做一致性读,或者叫做快照读。所有的普通 Select 语句在 RR 和 RC 隔离级别下都是一致性读。一致性读并不会对表中的任何记录加锁,其他事务可以自由对表的记录做改动。
1、共享锁和独占锁
共享锁:英文名: Shared Locks
,简称 S
锁 。在事务要读取一条记录时,需要先获取该记录的 S 锁 。
独占锁 :也常称排他锁 ,英文名: Exclusive Locks
,简称 X
锁 。在事务要改动一条记录时,需要先获
取该记录的 X 锁 。
假如事务 T1 首先获取了一条记录的 S 锁 之后,事务 T2 接着也要访问这条记录:
- 如果事务 T2 想要获取这个记录的 S 锁,那么事务 T2 也会获得该锁,说明 T1 和 T2 可以同时获取一条记录的 S 锁。
- 如果事务 T2 想要获取这个记录的 X锁,那么事务 T2 就会阻塞,说明 T1 和 T2 不可以同时获取一条记录的 S 锁和 X 锁。
假如事务 T1 首先获取了一条记录的 X 锁 之后,其他事务既不能获取这条记录的 S 锁也不能获取这条记录的 X 锁,此时其他事务的线程就会阻塞直到事务 T1 提交后释放掉 X 锁。
综上:S 锁 和 S 锁是兼容的, S 锁 和 X 锁是不兼容的, X 锁 和 X 锁也是不兼容的 。
兼容性 | X | S |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
锁定读的语句:
我们在读取一条记录的时候会加 S锁,其实我们要是在读取的时候不希望别的事务来读写该记录,我们也可以在读取的时候给这条记录加 X 锁,语句格式如下:
加 S 锁:
SELECT ... LOCK IN SHARE MODE;
如果当前事务用该语句给一条记录加了 S 锁,那么其他事务可以给该记录加 S 锁,但是不能加 X 锁;如果想要获取该记录的 X 锁,必须等待当前事务提交后释放掉 S 锁。
加 X 锁:
SELECT ... FOR UPDATE;
如果当前事务用该语句给一条记录加了 X 锁,那么其他事务既不能给该记录加 X 锁也不能给该记录加 S 锁;如果想要获取该记录的 X 锁或 S 锁,必须等待当前事务提交后释放掉 X 锁。
2、多粒度锁
其实我们上面提到的锁都是基于记录来的,就是对一条记录加锁,我们把它叫做行锁,这种锁的粒度比较洗细,作用范围是一行记录。其实呢,一个事务还可以对整个表加锁,我们叫做表锁,它的粒度比较粗,它也分为共享锁(S 锁)和独占锁(X 锁),和行级锁一样,假如:
- 一个事务给一个表加 S锁:
- 别的事务可以继续获取该表的 S 锁
- 别的事务可以继续获得该表中的某些记录的 S 锁
- 别的事务不可以继续获得该表的 X 锁
- 别的事务不可以继续获得该表中的某些记录的 X 锁
- 一个事务给一个表加 X 锁:
- 别的事务不可以继续获取该表的 S 锁
- 别的事务不可以继续获得该表中的某些记录的 S 锁
- 别的事务不可以继续获得该表的 X 锁
- 别的事务不可以继续获得该表中的某些记录的 X 锁
看到这里相信大家都明白了吧,其实表锁就是对数据库中的整张表加锁。
现在假如我们有一个事务对想对整个表加表级的 S 锁,我们需要确保这个表的每条记录没有被其他事务加 X 锁,因此我们需要遍历这个表中的所有记录,看是否有其他事务给表中的某条记录加了 X 锁,其实这样遍历所有的记录效率是非常低的。那有没有更好的办法呢?🤔🤔🤔
其实是有的🤪🤪🤪,哈哈哈!这时候我们的意向锁就登场了。那啥是意向锁呢?意向锁也是可以分为意向共享锁和意向独占锁的:
- 意向共享锁:英文名: Intention Shared Lock ,简称 IS锁 。当事务准备在某条记录上加 S 锁时,需要先在表级别加一个 IS 锁 。
- 意向独占锁:英文名: Intention Exclusive Lock ,简称 IX 锁 。当事务准备在某条记录上加 X 锁时,需要先在表级别加一个 IX 锁。
我们继续分析上面的问题,当我们的事务给一个表中的一条记录加 X 锁时,这个事务也需要对整个表加一个 IX 锁(对的,意向锁是表锁),此时当另一个事务准备给这个表加表级的 S 锁时,就不需要像上面那样去遍历整个表了,只需要检查这个表是否被加了 IX 锁,加了就说明此时有其他事务给表中的记录加了 X 锁,此时这个事务的线程只能阻塞,等待其他事务提交释放掉 X 锁,没有加的话直接加表级的 S 锁就行了。所以,意向锁的作用到这里就很明显了吧,其实就是可以快速判断是否有事务给这个表中的记录加了 X 锁或 S 锁。
最后,我们来看看意向锁和其他锁之间的互斥关系吧:
兼容性 | X | IX | S | IS |
---|---|---|---|---|
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
IX | 不兼容 | 兼容 | 不兼容 | 兼容 |
S | 不兼容 | 不兼容 | 兼容 | 兼容 |
IS | 不兼容 | 兼容 | 兼容 | 兼容 |
3、InnoDB 中的行锁
关于 InnoDB中的表锁比较简单,我们这里重点讲解 InnoDB 中的行锁。
行锁,顾名思义就是对数据库中的一条记录加锁,这么说很简单,其实,行锁一点都不简单。行锁呢又分为 Record Lock、Gap Lock 和 Next-Key Lock 。
Record Lock
为了我们方便讲解,我新建了一个表:
+--------+---------+---------+
| number | name | country |
+--------+---------+---------+
| 1 | l刘备 | 蜀 |
| 3 | z诸葛亮 | 蜀 |
| 8 | c曹操 | 魏 |
| 15 | x荀彧 | 魏 |
| 20 | s孙权 | 吴 |
+--------+---------+---------+
聚簇索引图如下:
记录锁是有 S 锁 和 X 锁 之分的,我们分别称之为 S 型记录锁 和 X 型记录锁,当一个事务获取了一条记录的 S 型记录锁后,其他事务也可以继续获取该记录的 S 型记录锁 ,但不可以继续获取 X 型正经记录锁 ;当一个事务获取了一条记录的 X 型记录锁后,其他事务既不可以继续获取该记录的 S 型记录锁 ,也不可以继续获取 X 型记录锁。如图:
图中就表示了对索引为 8 这列的数据加了一个 Record Locks。
Gap Lock
我们说 MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的(该隔离级别不能单独解决幻读问题),解决方案有两种,可以使用 MVCC 方案解决,也可以采用加锁方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁 。那我们怎么办呢?🤭🤭🤭其实官方提出了一种称之为 Gap Lock 的锁,官方的类型名称为: LOCK_GAP ,我们也可以简称为 gap 锁 。比方说我们把 number 值为 8 的那条记录加一个 gap锁 的示意图如下:
如图中为 number 值为 8 的记录加了 gap 锁 ,意味着不允许别的事务在 number 值为 8 的记录前边的间隙插入新记录,其实就是 number 列的值 (3, 8) 这个区间的新记录是不允许立即插入的。比方说有另外一个事务再想插入一条 number 值为 4 的新记录,它定位到该条新记录的下一条记录的 number 值为 8,而这条记录上又有一个 gap 锁 ,所以就会阻塞插入操作,直到拥有这个 gap 锁的事务提交了之后, number 列的值在区间 (3, 8) 中的新记录才可以被插入。
这个 gap 锁的提出仅仅是为了防止插入幻影记录而提出的,虽然有共享 gap 锁和独占 gap 锁这样的说法,但是它们起到的作用都是相同的。而且如果你对一条记录加了 gap 锁 (不论是共享 gap 锁还是独占 gap 锁 ),并不会限制其他事务对这条记录加正经记录锁或者继续加 gap 锁 ,gap 锁的作用仅仅是为了防止插入幻影记录而已。
但是此时有一个问题,我们如何给最后一条记录的后面的间隙加锁了,这就要用到数据页的知识了。数据页其实是有两条伪记录的:
- Infimum 记录,表示该页面中最小的记录。
- Supremum 记录,表示该页面中最大的记录
为了实现阻止其他事务插入 number 值在 (20, +∞) 这个区间的新记录,我们可以给索引中的最后一条记录,也就是 number 值为 20 的那条记录所在页面的 Supremum 记录加上一个 gap锁 ,画个图就是这样:
这样就可以阻止其他事务插入 number 值在 (20, +∞) 这个区间的新记录
Next-Key Lock
Next-Key Locks 其实也很简单,就是 Record Locks + Gap Locks,它既可以锁住一条记录也可以同时锁住这条记录前面的间隙。如图所示:
Insert Intention Lock
我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的 gap 锁 ( next-key 锁也包含 gap 锁 ,后边就不强调了),如果有的话,插入操作需要等待,直到拥有 gap 锁的那个事务提交。事务在等待的时候也需要在内存中生成一个锁结构 ,表明有事务想在某个间隙中插入新记录,但是现在在等待。其实这种类型的锁命名为 Insert IntentionLock ,官方的类型名称为LOCK_INSERT_INTENTION,我们也可以称为插入意向锁 。
假如我们把 number 值为 8 的那条记录加一个 插入意向锁 的示意图如下 :
现在,我们举一个例子来看看插入意向锁的功能,比方说现在 T1 为number 值为 8 的记录加了一个 gap 锁 ,然后T2 和 T3 分别想向 hero 表中插入 number 值分别为 4 、 5 的两条记录,所以现在为 number 值为 8 的记录加的锁的示意图就如下所示:
从图中可以看到,由于 T1 持有 gap 锁 ,所以 T2 和 T3 需要生成一个 插入意向锁的锁结构并且处于等待状态。当T1 提交后会把它获取到的锁都释放掉,这样 T2 和 T3 就能获取到对应的插入意向锁了(本质上就是把插入意向锁对应锁结构的 is_waiting 属性改为 false ), T2 和 T3 之间也并不会相互阻塞,它们可以同时获取到 number 值为 8的插入意向锁 ,然后执行插入操作。事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
到这里呢,我们 MySQL 锁讲的就差不多了,如果还有其他问题,其实我也不会🤣🤣🤣
巨人的肩膀: