本篇已收录在 MySQL 是怎样运行的 学习笔记系列
共享锁和独占锁
共享锁,英文名:Shared Locks,简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。
独占锁,也常称排他锁,英文名:Exclusive Locks,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁。
假如事务T1首先获取了一条记录的S锁之后,事务T2接着也要访问这条记录:
- 如果事务T2想要再获取一个记录的S锁,那么事务T2也会获得该锁,也就意味着事务T1和T2在该记录上同时持有S锁。
- 如果事务T2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务T1提交之后将S锁释放掉。
如果事务T1首先获取了一条记录的X锁之后,那么不管事务T2接着想获取该记录的S锁还是X锁都会被阻塞,直到事务T1提交。
所以我们说S锁和S锁是兼容的,S锁和X锁是不兼容的,X锁和X锁也是不兼容的,画个表表示一下就是这样:
锁定读的语句
对读取的记录加S锁:
SELECT ... LOCK IN SHARE MODE;
也就是在普通的SELECT语句后边加LOCK IN SHARE MODE,如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的S锁(比方说别的事务也使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录),但是不能获取这些记录的X锁(比方说使用SELECT ... FOR UPDATE语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的S锁释放掉。
对读取的记录加X锁:
SELECT ... FOR UPDATE;
也就是在普通的SELECT语句后边加FOR UPDATE,如果当前事务执行了该语句,那么它会为读取到的记录加X锁,这样既不允许别的事务获取这些记录的S锁(比方说别的事务使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录),也不允许获取这些记录的X锁(比方也说使用SELECT ... FOR UPDATE语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的S锁或者X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的X锁释放掉。
多粒度锁
我们前边提到的锁都是针对记录的,也可以被称之为行级锁或者行锁,对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁,自然就被称之为表级锁或者表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。给表加的锁也可以分为共享锁(S锁)和独占锁(X锁):
意向共享锁
意向共享锁,英文名:Intention Shared Lock,简称IS锁。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
意向独占锁
英文名:Intention Exclusive Lock,简称IX锁。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
小结
IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。我们画个表来看一下表级别的各种锁的兼容性:
InnoDB存储引擎中的锁
常用的行锁类型
Record Locks
这个锁就是前面讲的行锁.
Gap Locks
MySQL在REPEATABLE READ隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC方案解决,也可以采用加锁方案解决。
mysql 提出了一种称之为Gap Locks的锁,官方的类型名称为:LOCK_GAP,我们也可以简称为gap锁。
如图中为number值为8的记录加了gap锁,意味着不允许别的事务在number值为8的记录前边的间隙插入新记录,其实就是number列的值(3, 8)这个区间的新记录是不允许立即插入的。比方说有另外一个事务再想插入一条number值为4的新记录,它定位到该条新记录的下一条记录的number值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,number列的值在区间(3, 8)中的新记录才可以被插入。
为了实现阻止其他事务插入number值在(20, +∞)这个区间的新记录,我们可以给索引中的最后一条记录,也就是number值为20的那条记录所在页面的Supremum记录加上一个gap锁,画个图就是这样:
Next-Key Locks
有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,所以 InnoDB 就提出了一种称之为 Next-Key Locks 的锁,官方的类型名称为:LOCK_ORDINARY,我们也可以简称为next-key锁。next-key锁的本质就是一个正经记录锁和一个gap锁的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙。
InnoDB锁的内存结构
我们前边说对一条记录加锁的本质就是在内存中创建一个锁结构与之关联,那么是不是一个事务对多条记录加锁,就要创建多个锁结构呢?比方说事务T1要执行下边这个语句:
# 事务T1
SELECT * FROM hero LOCK IN SHARE MODE;
很显然这条语句需要为hero表中的所有记录进行加锁,那是不是需要为每条记录都生成一个锁结构呢?其实理论上创建多个锁结构没问题,反而更容易理解,但是谁知道你在一个事务里想对多少记录加锁呢,如果一个事务要获取10000条记录的锁,要生成10000个这样的结构也太亏了吧!所以 InnoDB 决定在对不同记录加锁时,如果符合下边这些条件:
- 在同一个事务中进行加锁操作
- 被加锁的记录在同一个页面中
- 加锁的类型是一样的
- 等待状态是一样的
那么这些记录的锁就可以被放到一个锁结构中。
锁所在的事务信息:
不论是表锁还是行锁,都是在事务执行过程中生成的,哪个事务生成了这个锁结构,这里就记载着这个事务的信息。
索引信息:
对于行锁来说,需要记录一下加锁的记录是属于哪个索引的。
表锁/行锁信息:
表锁结构和行锁结构在这个位置的内容是不同的:
-
表锁:
- 记载着这是对哪个表加的锁,还有其他的一些信息。
-
行锁:
-
记载了三个重要的信息:
- Space ID:记录所在表空间。
- Page Number:记录所在页号。
- n_bits:对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个n_bits属性代表使用了多少比特位。
-
-
type_mode:
-
这是一个32位的数,被分成了lock_mode、lock_type和rec_lock_type三个部分,如图所示:
-
锁的模式(lock_mode),占用低4位,可选的值如下:
- LOCK_IS(十进制的0):表示共享意向锁,也就是IS锁。
- LOCK_IX(十进制的1):表示独占意向锁,也就是IX锁。
- LOCK_S(十进制的2):表示共享锁,也就是S锁。
- LOCK_X(十进制的3):表示独占锁,也就是X锁。
- LOCK_AUTO_INC(十进制的4):表示AUTO-INC锁。
-
锁的类型(lock_type),占用第5~8位,不过现阶段只有第5位和第6位被使用:
- LOCK_TABLE(十进制的16),也就是当第5个比特位置为1时,表示表级锁。
- LOCK_REC(十进制的32),也就是当第6个比特位置为1时,表示行级锁。
-
行锁的具体类型(rec_lock_type),使用其余的位来表示。只有在lock_type的值为LOCK_REC时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:
- LOCK_ORDINARY(十进制的0):表示next-key锁。
- LOCK_GAP(十进制的512):也就是当第10个比特位置为1时,表示gap锁。
- LOCK_REC_NOT_GAP(十进制的1024):也就是当第11个比特位置为1时,表示正经记录锁。
- ...
-
-