vlambda博客
学习文章列表

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

从这篇文章起,就开始正式进入重点知识——“InnoDB存储引擎层的锁” 相关内容的学习。

锁系列第一篇文章就说过,『锁』是整个MySQL知识体系中较难理解的。在整理文章写作顺序的时候,也没有想到一个很清晰的写作脉络,可以从锁粒度(从行级别锁→表级别锁)的角度出发去写,也可以从加锁顺序(表级别锁→行级别锁)角度出发去写。但是,不同锁模式间的关系是“关联、并非独立、相辅相成的”,所以导致直到下笔,也没有想明白应该从什么地方开始写。
然而,大家不用担心。虽然锁的资料五花八门,但我还是会用通俗易懂的话去描述清楚每个知识点,最后知识的整理工作就交给大家自己了,毕竟,一千个读者就有一千个哈姆雷特。话不多说,开始今天的内容吧!~

S(共享锁)和X(排他/独占锁)




01

准备工作

PREPARE

为了方便说明,我们先做一些准备工作,首先创建一张测试表,并插入一些测试数据:
mysql> USE `test_lock`;

mysql> CREATE TABLE `l` (
`a` INT(11) NOT NULL,
`b` INT(11) DEFAULT NULL,
`c` INT(11) DEFAULT NULL,
`d` INT(11) DEFAULT NULL,
PRIMARY KEY (`a`),
KEY `idx_b` (`b`),
UNIQUE KEY `uniq_c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

mysql> INSERT INTO `l` VALUES(2,4,6,8),(4,6,8,10),(6,8,10,12),(8,10,12,14);

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

我们创建了一张存储引擎为InnoDB,表名为l的表,a列为主键b列为普通二级索引c列为唯一索引d没有索引

为了方便查看锁相关信息,我们还需要打开一个参数(见名知意:InnoDB锁状态输出):

mysql> SET GLOBAL innodb_status_output_locks='ON';
mysql> SHOW VARIABLES LIKE '%innodb%lock%';

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁


02

共享锁和排他锁

S & X LOCK

共享锁(S)和排他锁(X)都属于行级别(row-level)的锁,且都比较好理解,S锁与X锁互相冲突,X锁与X锁也是互相冲突,S锁与S锁不会冲突。

  • 当读取一行记录时,为了防止别人修改,需要添加S锁

  • 当修改一行记录时,为了防止别人同时进行修改,需要添加X锁

Lock(锁)就是用于管理对共享资源的并发访问的。所以,现在再来看这个X锁和S锁的兼容矩阵,是不是就很好理解了。

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

从事MySQL相关工作的小伙伴都知道MySQL的多版本并发控制(MVCC,Mutil-Version Concurrency Control),虽然目前还没有学这部分知识,不要着急,等我MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁。在通常情况下,普通的查询属于非锁定读,不会添加任何锁(即一致性读)。还有一种是锁定读(即当前读),那么以下两种情况会触发查询操作的锁定读(当前读)

  • SELECT ... LOCK IN SHARE MODE; 这个语法会添加S锁,对于其他事务可以读但不可以修改

  • SELECT ... FOR UPDATE; 这个语法会添加X锁,其他事务修改或者执行SELECT ... FOR UPDATE操作都会被阻塞。

验证一下:

# Transaction 1:
mysql> BEGIN;
mysql> SELECT * FROM test_lock.l WHERE a=2 LOCK IN SHARE MODE;

# Transaction 2:
mysql> BEGIN;
mysql> SELECT * FROM test_lock.l WHERE a=2 LOCK IN SHARE MODE;
mysql> SELECT * FROM test_lock.l WHERE a=2 FOR UPDATE;

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

Transaction 2中的SELECT ... LOCK IN SHARE MODE没有被阻塞,SELECT ... FOR UPDATE语句被阻塞了。

03

查看持有锁的信息

LOCK INFO

综上,锁冲突发生时,另一个事务执行的SQL就会被阻塞,下面就来看下如何定位锁的相关信息(在上述SELECT ... FOR UPDATE语句被阻塞的同时,开启另外一个Session,执行命令,“TRANSACTIONS”信息栏下):

mysql> SHOW ENGINE INNODB STATUS\G

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

如图红框所示:
  • lock mode S :代表S(共享)锁;
  • lock_mode X :代表X(排他)锁;

额外补充一点,锁相关内容需要配合事务隔离级别来看,上面的例子我们已经提前将当前我们的环境配置成READ COMMITTED(读已提交)事务隔离级别:

mysql> SET tx_isolation='READ-COMMITTED';
mysql> SELECT @@tx_isolation;

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

继续,除了看到锁类型外,还有很多输出信息,我们一一来看一下:

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

  • 第1行:LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s):表示现在整个实例下总共有4个锁结构,堆大小为1136,2个行锁;

  • 第2行:MySQL thread id 9, OS thread handle 140510686721792, query id 15314 localhost root statistics:MySQL线程、操作系统线程等信息;

  • 第3行:SELECT * FROM test_lock.l WHERE a=2 FOR UPDATE:当前发生阻塞的SQL语句;

  • 第4行:------- TRX HAS BEEN WAITING 189 SEC FOR THIS LOCK TO BE GRANTED::表示当前的SQL为了获取(以下内容)锁已被阻塞189秒;

  • 第5行:RECORD LOCKS space id 142 page no 3 n bits 72 index PRIMARY of table `test_lock`.`l` trx id 3373864 lock_mode X locks rec but not gap waiting:描述了当前SQL的要获取的锁信息,当前SQL申请的锁粒度是RECORD LOCKS(记录锁),要申请锁的这行记录在space id(表空间编号)为142,page no(页号)为3,在表`test_lock`.`l`上的index PRIMARY(聚簇索引)上,锁的类型是X(排他)锁,waiting表示当前这个锁正在等待被GRANTED(被获取)。

  • 第6行:Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0:记录锁,当前记录所在的heap no(当前记录在记录物理堆的位置信息)为2,n_fields(记录中列的数量)总共有6列,记录的format(行格式)为compact。

  • 第7行至第12行:这些就是加锁记录的完整信息。

 0: len 4; hex 80000002; asc     ;;
1: len 6; hex 000000337b1d; asc 3{ ;;
2: len 7; hex b7000000980110; asc ;;
3: len 4; hex 80000004; asc ;;
4: len 4; hex 80000006; asc ;;
5: len 4; hex 80000008; asc ;;
  • 第1列列编号,0表示第一列,1表示第二列,后面依次类推;

  • 第2列字段长度,单位字节(B),len 4表示的是隐藏列DB_ROW_ID,因为我们l表定义了PRIMARY KEY(PK)主键,所以row_id就是PK,因为定义的是INT类型,所以长度4字节;len 6表示的是隐藏列DB_TRX_ID(事务ID),长度6字节;len 7表示的是DB_ROLL_PTR(回滚指针),长度7字节;后面的依次类推,这样就可以理解上面n_fields为什么有6列了;

  • 第3列完整的记录值,是以hex(十六进制方式)显示的,80000002 → 8表示该字段类型是符号的(值为1,代表有符号),000000337bid → 0表示该字段类型是符号的(值为0,代表无符号),80000002 → 2就表示第一个值是2,后面的值分别是4、6、8,也就是我们表第一行的完整的记录。

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

  • 第4列记录排序方式,asc(升序,默认值)。

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

后面的信息主要显示了哪些记录上还加了什么类型的锁,相同的内容解析同上,不再赘述。看红框所示的内容,熟不熟悉,TABLE LOCK ... lock mode IS、TABLE LOCK ... lock mode IX,没错,就是我们说的IS(意向共享)锁和IX(意向排他)锁,为了文章条理性,放在下一模块马上介绍。

到现在我们已经会查看锁的相关信息了,以上这部分内容非常重要,希望大家都能掌握在日常工作中对分析锁时和后面的内容都非常有用,虽然不好理解,但是,书读百遍其义自见,大家多看几遍自然就懂了,毕竟写的这么通俗易懂MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

小提示

其实看到这里,有木有发现,相关内容用到了很多之前学习的知识,如果大家有所遗忘,快去考个古。由此观之,InnoDB存储引擎层的锁是加在索引(记录)上的。

n_fields(记录中列的数量)总共有6列,编号是从0开始直到5,包含了隐藏列。

page no(页号)为3,InnoDB存储引擎中,page no为3的页(也就是每个表空间的第4个页)所有真实的记录都是从这个页开始写,也是聚簇索引整个B+树的root根节点。那么前3个页是干什么的呢?忘记的可以移步至。




IS(意向共享锁)和IX(意向排他锁)





01

InnoDB存储引擎层加锁顺序

LOCK SEQUENCE

S锁和X锁都是行级别(rwo-level)的行锁 是加在索引(记录)上的 兼容与否是指对同一条记录(row)来说的
从上面的例子中,锁内容输出中我们也看到了“TABLE LOCK ...”,这就说明, InnoDB存储引擎是支持多粒度(granular)锁定的 这种锁定允许事务在行(row-level)级别上和表(table-level)级别上同时存在 。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎 支持一种额外的锁方式 ,称之为 意向锁(INTENTION LOCK) 。意向锁是将锁定的对象分为多个层次,意味着事务希望在更细粒度(fine granularity)上进行加锁。
若将上锁的对象看成一棵树(就像我们B+树存储结构那样,一个根节点,多个分支节点和叶子节点),如果对最下层的对象(可以理解成leaf叶子节点)加锁,也就是说对最细粒度的对象进行加锁,那么首先需要对粗粒度的对象上锁,如图所示。

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

如果需要对Page(页)上的记录r加X锁,那么分别需要对数据库D、表、Page(页)上先加IX(意向排他锁),最后对记录r上加X锁。也就是说, 在InnoDB存储引擎中 给r这行记录加锁 需要4个粒度的锁 加锁顺序就是:先给库级别加IX锁 → 表级别加IX锁 → 页级别加IX锁 → 记录上加X锁 。若其中任何一部分导致等待(阻塞),那么该操作就需要等待粗粒度锁的完成。再来回顾一下官网( https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html )提供的锁兼容矩阵(Conflict-冲突,Compatible-兼容)。

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

再举个栗子🌰说明一下: 对记录r加X锁之前, 如果已经有事务对表1 加了S 级别(table-level ,那么表1上已存在 S锁 ,之后事务 需要对记录 r在表1 上加IX锁 ,由于不兼容, 所以该事务 需要等待表级别锁 操作完成。

02

意向锁详解

DETAIL

在MySQL官档的描述中,有这样一句话:

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

意向锁是表级别(table-level)的锁 ,由此也能看出InnoDB存储引擎对意向锁的设计是比较简练的,没有如全局级别、库级别、Page级别(粒度)的说法。
意向锁其实设计的主要目的就是:为了在一个事务中揭示下一层将被请求的锁类型 。同样,支持两种意向锁:
  • 意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁

  • 意向排他锁(IX Lock):事务想要获得一张表中某几行的排他锁

针对上面官网中对意向锁的描述,其实是不完整的,其实第一模块讲S锁和X锁的那个例子,我们不难看出,我们a=2这 一行记录加锁 也会出现意向锁 的身影。所以,这点大家需要注意。
”意向锁其实设计的主要目的就是:为了在一个事务中揭示下一层将被请求的锁类型“,这句话,大家有没有“ 所有字都认识,就是不知道是什么意思 ”的感觉MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁?那么下面我来给翻译翻译:
意向锁,见名知意,就是有加锁意向,再来回顾加锁的顺序:先给库级别加IX锁 → 表级别加IX锁 → 页级别加IX锁 → 记录上加X锁。揭示下一层级请求的锁类型,对当前层级是没有关系的,就是说比如想要给表1加一个表级别的S锁。先给库级别加IS锁(IS和IX不冲突,可以正常加锁) → 表1加S锁(但是S和IX不兼容),发生锁等待。
问:数据库下一层级不是表吗,IX下一层级为什么还是IX?
答:因为InnoDB只定义了表(table-level)级别的意向锁,但是最终的锁结构是实现在记录级的。而且加锁逻辑是不能跳过层级直接加到记录级,是需要一层一层往下加,从上往下,像一棵树一样一层一层加锁。同时意向锁只揭示了下一层将被请求的锁类型,所以可以理解为当前层级及以上层级都“只是揭示”加的是意向锁,且意向锁之间都兼容的。
总结一下:一个对象进行加锁,哪怕这个对象是最低级别的记录锁,那么他也是从上往下,像树一样一层一层往下加锁的。S和X可以在多个粒度级别上面进行加,意向锁表示的是下一层级要加什么锁,对于当前这个层级,大家都是互相兼容的(大家加的都是锁的意向【有想法,但还未落实,落实的事情交给下一层手下的人去做】),所以锁兼容矩阵中 → 意向锁之间都是互相兼容的
是不是瞬间顿悟了MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁
小提示
还记得上篇文章埋下的坑吗?

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

官档中说:意向锁都是table-level locks。这样说的原因是:InnoDB存储引擎不像MySQL Server层的MDL锁,锁住的对象直接就是表这个对象。虽然有库、表、页、记录4个层级(粒度),但是在InnoDB存储引擎中,是没有数据库、数据页级别的锁的,这就意味着,对于InnoDB存储引擎来说,相当于所有的意向锁都是加在表级别上面的(而非表这个对象),所以就称之为它为表(table-level)级别锁,这个结论是通过反向推导得来的,这也就理解了官档中“InnoDB支持标准的行级锁,意向锁是表锁的”原因。

说了这么多,举个栗子🌰看一下IX锁:
mysql> BEGIN;
mysql> DELETE FROM test_lock.l WHERE a=2;

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

mysql> SHOW ENGINE INNODB STATUS\G

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

看到锁输出的信息, 2 lock struct(s) :两个锁结构,一个是加在表l上的 lock mode IX 的意向排他锁,一个是加在a=2这条记录上 lock_mode X locks rec 的排他记录锁。 1 row lock(s) :一行记录被锁住。
整个过程就是,要删除a=2这条记录了,需要先在表l上加一个意向排他锁IX,然后获取这行记录的X锁,同时告诉别的事务,我要删除这条记录了,锁在我这里,你们就不要妄想动这条记录了,啥时候我提交或回滚事务,就将这把锁释放掉(这条记录就和我没有关系了)。



小结




今天我们学习了共享锁(S)、排他锁(X)、意向共享锁(IS)和意向排他锁(IX),下面简单做一下总结:
  • 掌握如何解读持有锁的信息,如多少个锁结构、多少行锁、锁住的记录信息(n_fields、page no 3代表的含义)、锁的类型等;

  • InnoDB存储引擎层的加锁顺序是:库 → 表 → 页 → 记录;

  • InnoDB存储引擎设计意向锁的主要目的就是:为了在一个事务中揭示下一层将被请求的锁类型。即意向锁表示的是下一层级要加什么锁,对于当前这个层级,大家都是互相兼容的(大家加的都是锁的意向【有想法,但还未落实,落实的事情交给下一层手下的人去做】),所以锁兼容矩阵中 → 意向锁之间都是互相兼容的。

  • MySQL Server层MDL锁的表锁和InnoDB存储引擎意向锁的表(table-level)锁的区别;

  • 掌握锁兼容矩阵,分清MySQL Server层MDL元数据锁和InnoDB存储引擎层的锁区别。

其实InnoDB存储引擎层的锁实现比想象中的要复杂的多,因个人水平有限,不能配合源码来更深层次的解读,望大家见谅MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

今天的文章我用比较通俗易懂的话讲解了InnoDB存储引擎层加锁顺序和原理,也详细的介绍了持有锁信息的解读,这些内容都非常重要。本来打算写Record Lock(记录锁)、Gap Lock(间隙锁)和Next-key Lock(临键锁),出于篇幅和锁知识难理解的原因,还是将这部分拆分成下一篇文章,这样也可以让大家更好的吸收这部分知识。

希望今天文章的书写顺序能顺应大家的“阅读口味”,每天进步一点点,希望今天的文章能帮助到每一位小伙伴!~




MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁
MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁
MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

参考资料

  • 姜承尧《MySQL技术内幕:InnoDB存储引擎 第2版》

  • 姜承尧《MySQL内核:InnoDB存储引擎 卷I》

  • 李春、罗小波、董红禹《千金良方:MySQL 性能优化金字塔法则》

  • https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁
MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁


MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁
MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁
MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁

扫描二维码关注

获取更多精彩

GrowthDBA

MySQL之锁详解(二):InnoDB的S锁、X锁、IS锁和IX锁


end