首页/应用软件/内容

MySQL数据库InnoDB引擎行级锁锁定范围详细说明

应用软件2022-09-15 阅读()
SQL是Structured Query Language(结构化查询语言)的缩写。SQL是专为数据库而建立的操作命令集,是一种功能齐全的数据库语言。在使用它时,只需要发出“做什么”的命令,“怎么做”是不用使用者考虑的。SQL功能强大、简单易学、使用方便,已经成为了数据库操作的基础,并且现在几乎所有的数据库均支持SQL。
Mysql数据库InnoDB引擎支持行级锁,也就是说我们可以对表中某些行数据执行锁定操作,锁定操作的影响是:如果一个事物对表中某行执行了锁定操作,而另一个事务也需要对同样的行执行锁定操作,这样第二个事务的锁定操作有可能被阻塞,一旦被阻塞第二个事务只能等到第一个事务执行完毕(提交或回滚)或超时。

本文主要介绍InnoDB中的行锁相关概念,重点介绍行锁的锁定范围:

背景知识

上面我们简单的介绍了InnoDB的行级锁,为了理解后面的验证部分,需要补充一下背景知识。如果对相应知识非常了解,可以直接跳转到验证部分内容。

1. InnoDB锁的类型

InnoDB引擎使用了七种类型的锁,他们分别是:

本文主要涉及Shared and Exclusive Locks,Record Locks,Gap Locks,Next-Key Locks这几种锁,其他类型锁如果大家感兴趣可以自己深入了解,在此不在详述。

1.1 Shared and Exclusive Locks

共享锁(S锁)和排他锁(X锁)的概念在许多编程语言中都出现过。先来描述一下这两种锁在MySQL中的影响结果:

用一张经典的矩阵表格继续说明共享锁和排他锁的互斥关系:

--SX
S01
X11

图中S表示共享锁X表示独占锁,0表示锁兼容1表示锁冲突,兼容不被阻塞,冲突被阻塞。由表可知一旦一个事务加了排他锁,其他个事务加任何锁都需要等待。多个共享锁不会相互阻塞。

1.2 Record Locks、Gap Locks、Next-Key Locks

这三种类型的锁都描述了锁定的范围,故放在一起说明。

以下定义摘自MySQL官方文档

  • 记录锁(Record Locks):记录锁锁定索引中一条记录。

  • 间隙锁(Gap Locks):间隙锁要么锁住索引记录中间的值,要么锁住第一个索引记录前面的值或者最后一个索引记录后面的值。

  • Next-Key Locks:Next-Key锁是索引记录上的记录锁和在索引记录之前的间隙锁的组合。

定义中都提到了索引记录(index record)。为什么?行锁和索引有什么关系呢?其实,InnoDB是通过搜索或者扫描表中索引来完成加锁操作,InnoDB会为他遇到的每一个索引数据加上共享锁或排他锁。所以我们可以称行级锁(row-level locks)为索引记录锁(index-record locks),因为行级锁是添加到行对应的索引上的。

三种类型锁的锁定范围不同,且逐渐扩大。我们来举一个例子来简要说明各种锁的锁定范围,假设表t中索引列有3、5、8、9四个数字值,根据官方文档的确定三种锁的锁定范围如下:

最后对于间隙锁还需要补充三点:

  1. 间隙锁阻止其他事务对间隙数据的并发插入,这样可有有效的解决幻读问题(Phantom Problem)。正因为如此,并不是所有事务隔离级别都使用间隙锁,MySQL InnoDB引擎只有在Repeatable Read(默认)隔离级别才使用间隙锁。

  2. 间隙锁的作用只是用来阻止其他事务在间隙中插入数据,他不会阻止其他事务拥有同样的的间隙锁。这就意味着,除了insert语句,允许其他SQL语句可以对同样的行加间隙锁而不会被阻塞

  3. 对于唯一索引的加锁行为,间隙锁就会失效,此时只有记录锁起作用

2. 加锁语句

前面我们已经介绍了InnoDB的是在SQL语句的执行过程中通过扫描索引记录的方式来实现加锁行为的。那哪些些语句会加锁?加什么样的锁?接下来我们逐一描述:

最后补充两点:

  1. 如果一个查询使用了辅助索引并且在索引记录加上了排他锁,InnoDB会在相对应的聚合索引记录上加锁。

  2. 如果你的SQL语句无法使用索引,这样MySQL必须扫描整个表以处理该语句,导致的结果就是表的每一行都会被锁定,并且阻止其他用户对该表的所有插入。

SQL语句验证

闲言少叙,接下来我们进入本文重点SQL语句验证部分。

1.测试环境

数据库:MySQL 5.6.35
事务隔离级别:Repeatable read
数据库访问终端:mysql client

2.验证场景

2.1 场景一

建表:

CREATE TABLE `user` (
 `id` int(11) NOT NULL,
 `name` varchar(8) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入数据:

INSERT INTO `user` (`id`, `name`) VALUES ('1', 'a');
INSERT INTO `user` (`id`, `name`) VALUES ('3', 'c');
INSERT INTO `user` (`id`, `name`) VALUES ('5', 'e');
INSERT INTO `user` (`id`, `name`) VALUES ('7', 'g');
INSERT INTO `user` (`id`, `name`) VALUES ('9', 'i');

首先我们执行SQL语句的模板:

步骤client 1client 2
1begin;--
2SELECT * FROM user where name='e' for update;--
3--begin;
4--INSERT INTO `user` (`id`, `name`) VALUES (10, #{name});
5rollback;--
6--rollback;

替换步骤5中name的值,观察结果:

name的值执行结果
a不阻塞
b不阻塞
d阻塞
e阻塞
f阻塞
h不阻塞
i不阻塞

观察结果,我们发现SQL语句
SELECT * FROM user where name='e' for update
一共锁住索引name中三行记录,(c,e]区间应该是next-key锁而(e,h)区间是索引记录e后面的间隙。

接下来我们确定next-key锁中哪部分是索引记录锁哪部分是间隙锁。

执行SQL语句的模板:

步骤client 1client 2
1begin;--
2SELECT * FROM user where name='e' for update;--
3--SELECT * FROM user where name=#{name} for update;
5rollback;--
6--rollback;

替换步骤5中name的值,观察结果:

name的值执行结果
d不阻塞
e阻塞
f不阻塞

因为间隙锁只会阻止insert语句,所以同样的索引数据,insert语句阻塞而select for update语句不阻塞的就是间隙锁,如果两条语句都阻塞就是索引记录锁。

观察执行结果可知,d和f为间隙锁,e为索引记录锁。

结论:通过两条SQL,我们确定了对于辅助索引name在查询条件为 where name='e' 时的加锁范围为(c,e],(e,g),其中:

说的这里细心的读者可能已经发现我们的测试数据中没有间隙的边界数据c和g。接下来我们就对间隙边界值进行测试。

执行SQL语句的模板:

步骤client 1client 2
1begin;--
2SELECT * FROM user where name='e' for update;--
3--begin;
4--INSERT INTO `user` (`id`, `name`) VALUES (#{id}, #{name});
5rollback;--
6--rollback;

替换步骤5中id,name的值,观察结果:

id的值name=c执行结果id的值name=g执行结果
-------3g组塞
-------2g阻塞
-1c不阻塞-1g阻塞
1c不阻塞1g不阻塞
2c不阻塞2g阻塞
3c不阻塞3g不阻塞
4c阻塞4g阻塞
5c阻塞5g阻塞
6c阻塞6g阻塞
7c不阻塞7g不阻塞
8c阻塞8g不阻塞
9c不阻塞9g不阻塞
10c阻塞10g不阻塞
11c阻塞---
12c阻塞---

通过观察以上执行结果,我们发现,name等于c和e时insert语句的结果随着id值得不同一会儿锁定,一会儿不锁定。那一定是id列加了锁才会造成这样的结果。

如果先不看id=5这一行数据的结果,我们发现一个规律:

接下来我们解释一下id=5的锁定情况

执行SQL语句的模板:

步骤client 1client 2
1begin;--
2SELECT * FROM user where name='e' for update;--
3--SELECT * FROM user where id=#{id} for update;
5rollback;--
6--rollback;

替换步骤5中id的值,观察结果:

id的值执行结果
3不阻塞
4不阻塞
5阻塞
6不阻塞
7 不阻塞

通过观察执行结果可知,id=5的聚合索引记录上添加了索引记录锁。根据MySQL官方文档描述,InnoDB引擎在对辅助索引加锁的时候,也会对辅助索引所在行所对应的聚合索引(主键)加锁。而主键是唯一索引,在对唯一索引加锁时,间隙锁失效,只使用索引记录锁。所以SELECT * FROM user where name='e' for update;不仅对辅助索引name=e列加上了next-key锁,还对对应的聚合索引id=5列加上了索引记录锁。

最终结论:
对于SELECT * FROM user where name='e' for update;一共有三种锁定行为:

  1. 对SQL语句扫描过的辅助索引记录行加上next-key锁(注意也锁住记录行之后的间隙)。

  2. 对辅助索引对应的聚合索引加上索引记录锁。

  3. 当辅助索引为间隙锁“最小”和“最大”值时,对聚合索引相应的行加间隙锁。“最小”锁定对应聚合索引之后的行间隙。“最大”值锁定对应聚合索引之前的行间隙。

上面我们将对辅助索引加锁的情况介绍完了,接下来我们测试一下对聚合索引和唯一索引加锁。

2.2 场景二

建表:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(8) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `index_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

注意与场景一表user不同的是name列为唯一索引。

插入数据:

INSERT INTO `user` (`id`, `name`) VALUES ('1', 'a');
INSERT INTO `user` (`id`, `name`) VALUES ('3', 'c');
INSERT INTO `user` (`id`, `name`) VALUES ('5', 'e');
INSERT INTO `user` (`id`, `name`) VALUES ('7', 'g');
INSERT INTO `user` (`id`, `name`) VALUES ('9', 'i');

首先我们执行SQL语句的模板:

步骤client 1client 2
1begin;--
2SELECT * FROM user where name='e' for update;
3--begin;
4--INSERT INTO `user` (`id`, `name`) VALUES (10, #{name});
5rollback;--
6--rollback;

替换步骤5中name的值,观察结果:

name的值执行结果
a不阻塞
b不阻塞
c不阻塞
d不阻塞
e阻塞
f不阻塞
g不阻塞
h不阻塞
i不阻塞

由测试结果可知,只有name='e'这行数据被锁定。

通过SQL语句我们验证了,对于唯一索引列加锁,间隙锁失效,

2.3 场景三

场景一和场景二都是在查询条件等于的情况下做出的范围判断,现在我们尝试一下其他查询条件,看看结论是否一致。

借用场景一的表和数据。

建表:

CREATE TABLE `user` (
  `id` int(11) NOT NULL,
  `name` varchar(8) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入数据:

INSERT INTO `user` (`id`, `name`) VALUES ('1', 'a');
INSERT INTO `user` (`id`, `name`) VALUES ('3', 'c');
INSERT INTO `user` (`id`, `name`) VALUES ('5', 'e');
INSERT INTO `user` (`id`, `name`) VALUES ('7', 'g');
INSERT INTO `user` (`id`, `name`) VALUES ('9', 'i');

执行SQL语句的模板:

步骤client 1client 2
1begin;--
2SELECT * FROM user where name>'e' for update;--
3--begin;
4--INSERT INTO `user` (`id`, `name`) VALUES ('10', #{name});
5rollback;--
6--rollback;

替换步骤5中name的值,观察结果:

name的值执行结果
a阻塞
b阻塞
c阻塞
d阻塞
e阻塞
f阻塞
g阻塞
h阻塞
i阻塞

这个结果是不是和你想象的不太一样,这个结果表明where name>'e'这个查询条件并不是锁住'e'列之后的数据,而锁住了所有name列中所有数据和间隙。这是为什么呢?

我们执行以下的SQL语句执行计划:

 explain select * from user where name>'e' for update;

执行结果:

+----+-------------+-------+-------+---------------+------------+---------+------+------+--------------------------+
(北联网教程,专业提供视频软件下载)

第1页  第2页  第3页  第4页  第5页  第6页  第7页  第8页  第9页  第10页  第11页  第12页  第13页  第14页  第15页  第16页  第17页  第18页  第19页  第20页  第21页  第22页  第23页  第24页  第25页  第26页  第27页  第28页  第29页  第30页  第31页  第32页  第33页  第34页  第35页  第36页  第37页  第38页  第39页  第40页  第41页  第42页  第43页  第44页  第45页 

……

相关阅读