MySQL-加锁规则(间隙锁临键锁行锁表锁)
Posted WSYW126
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL-加锁规则(间隙锁临键锁行锁表锁)相关的知识,希望对你有一定的参考价值。
环境
mysql version 5.6.47
隔离级别:可重复读(RR)
加锁规则
可重复读隔离级别下加锁规则
next-key lock = 间隙锁 + 行锁
原则1:加锁的基本单位是next-key lock。next-key lock是前开后闭区间。
原则2:查找过程中访问到的对象才会加锁。
原则3:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
原则4:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
原则5:唯一索引上的范围查询会访问到不满足条件的第一个值为止。【注释:其实我不认为这是bug,只是唯一索引和非唯一索引保持一致】
知识点
锁是加在索引上的而不是加在数据上
锁是加在索引上的而不是加在数据上
lock in share mode与for update的区别
区别一:
lock in share mode加的是读锁
for update 加的是写锁
区别二:
在非主键索引上通过两种方式加锁是有区别的。
lock in share mode 只锁覆盖索引,也就说:lock in share mode只锁非主键索引对应的B+树中的索引内容。
for update:如果对非主键索引使用 for update加锁就不一样了。 执行 for update 时,mysql会认为你接下来要更新数据,因此会通过非主键索引中的主键值继续在主键索引对应的b+数上查找到对应的主键索引项进行加锁,也就是说:for update的加锁内容是非主键索引树上符合条件的索引项,以及这些索引项对应的主键索引树上相应的索引项。在两个索引上都加了锁。
案例数据
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values (5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
id(主键) | c(普通索引) | d(无索引) |
5 | 5 | 5 |
10 | 10 | 10 |
15 | 15 | 15 |
20 | 20 | 20 |
25 | 25 | 25 |
以上数据为了解决幻读问题,更新的时候不只是对上述的五条数据增加行锁,还对于中间的取值范围增加了6个临键锁,(-∞,5](5,10](10,15](15,20](20,25](25,+supernum] (其中supernum是数据库维护的最大的值。为了保证临键锁都是左开右闭原则。)
案例一:间隙锁简单案例
步骤 | 事务A | 事务B |
1 | begin; select * from t where id = 11 for update; | - |
2 | - | insert into t value(10,10,10); //Duplicate entry '10' for key 'PRIMARY’ insert into t value(12,12,12) //blocked insert into t value(15,15,15) // Duplicate entry '15' for key 'PRIMARY' |
3 | commit; | - |
当有如下事务A和事务B时,事务A会对数据库表增加(10,15]这个区间锁,经过原则4,退化为间隙锁(10,15)。
这时insert id = 12 的数据的时候就会因为区间锁(10,15)而被锁住无法执行。
这时insert id = 10 和 insert id = 15的数据是未被锁定,报错:Duplicate entry '15' for key 'PRIMARY'
案例二: 间隙锁死锁问题
步骤 | 事务A | 事务B |
1 | begin; select * from t where id = 9 for update; | - |
2 | - | begin; select * from t where id = 6 for update; |
3 | - | insert into t value(7,7,7) //blocked
|
4 | insert into t value(7,7,7) //Deadlock found when trying to get lock; try restarting transaction
| - |
不同于写锁相互之间是互斥的原则,间隙锁之间不是互斥的。
如果一个事务A获取到了(5,10)之间的间隙锁,另一个事务B也可以获取到(5,10)之间的间隙锁。这时就可能会发生死锁问题。
如下案例:
事务A获取到(5,10)之间的间隙锁不允许其他的DDL操作,在事务提交,间隙锁释放之前,事务B也获取到了间隙锁(5,10),这时两个事务就处于死锁状态
案例三: 等值查询—唯一索引
步骤 | 事务A | 事务B |
1 | begin; update t set d= d+ 1 where id = 7; | - |
2 | - | insert into u (8,8,8); //blocked update t set d = d+ 1 where id = 10; //ok insert into t value(10,10,10); //Duplicate entry '10' for key 'PRIMARY' |
4 | - | - |
1.加锁的范围是(5,10]的临建锁
2.由于数据是等值查询,并且表中最后数据id = 10 不满足id= 7的查询要求,故id=10 的行级锁退化为间隙锁,(5,10)
执行说明:
1.事务B中id=8会被锁住,而insert id=10 和 update id=10的时候不都会被锁住。
案例四: 等值查询—普通索引
步骤 | 事务A | 事务B |
1 | begin; select id from t where c = 5 lock in share mode; | - |
2 | - | update t set d = d + 1 where id = 5; insert into t value(7,7,7); //blocked insert into t value (4,4,4); //blocked insert into t value(10,10,10); //Duplicate entry '10' for key 'PRIMARY' |
4 | - | - |
1.初步加锁,加锁的范围是(0,5],(5,10]的临建锁。
2.由于c是普通索引,根据原则4,搜索到5后继续向后遍历直到搜索到10才放弃,故加锁范围为(5,10]
3.根据原则4,由于查询是等值查询,并且最后一个值不满足查询要求,故间隙锁退化为(5,10)
4.最终的加锁范围是:(0,10)
执行说明:
1.因为加锁是对普通索引c加锁,而且因为索引覆盖,没有对主键进行加锁,所以事务B执行正常。
2.因为加锁范围(0,10),故insert id=4 和 update id=7执行阻塞。
3.需要注意的是,lock in share mode 因为覆盖索引故没有锁主键索引,如果使用for update 程序会觉得之后会执行更新操作故会将主键索引一同锁住。
案例五: 范围查询—唯一索引
步骤 | 事务A | 事务B |
1 | begin; select * from t where id >= 10 and id <11 for update | - |
2 | - | update t set d = d+ 1 where id = 15; //blocked insert into t value(8,8,8); //ok insert into t value(10,10,10); //blocked insert into t value(13,13,13); //blocked insert into t value (15,15,15); //blocked insert into t value (16,16,16); //ok |
4 | - | - |
- next-key lock 增加范围锁(5,10]
- 根据原则5,唯一索引的范围查询会到第一个不符合的值位置,故增加(10,15]
- 因为等值查询有id =10,根据原则3间隙锁升级为行锁,故剩余锁[10,15]
- 因为查询并不是等值查询,故[10,15]不会退化成[10,15)
- 最终的加锁范围是:[10,15]
执行说明:
1.insert id=8 和 update id=16,执行OK。
2.update id=15、insert id=10 和 insert id=13 和insert id=15, 执行阻塞。
案例六: 范围查询—普通索引
步骤 | 事务A | 事务B |
1 | begin; select * from t where c >= 10 and c <11 for update | - |
2 | - | update t set d = d+ 1 where c = 15; //blocked insert into t value(8,8,8); //blocked insert into t value(10,10,10); //blocked insert into t value(13,13,13); //blocked insert into t value (15,15,15); //Duplicate entry '15' for key 'PRIMARY' insert into t value (16,16,16); //ok |
4 | - | - |
- next-key lock 增加范围锁(5,10],(10,15]
- 因为c是非唯一索引,故(5,10]不会退化为10
- 因为查询并不是等值查询,故[10,15]不会退化成[10,15)
执行说明:
1.insert id=15 ,Duplicate entry '15' for key 'PRIMARY’。[存疑之处]
2.update id=16,执行OK。
3.update id=15、insert id=8 和 insert id=10 和 insert id=13,执行阻塞。
案例七: 普通索引-等值问题
上面的数据增加一行(30,10,30),这样在数据库中存在的c=10的就有两条记录
步骤 | 事务A | 事务B |
1 | begin; delete from t where c = 10 | - |
2 | - | update t set d = d+ 1 where c = 15; //ok insert into t value(12,12,12) //blocked insert into t value(5,5,5) // Duplicate entry '5' for key 'PRIMARY' insert into t value(6,6,6) //blocked
|
4 | - | - |
- next-key lock 增加范围锁(5,10],(10,15]
- 因为是等值查询故退化为(5,10],(10,15)
执行说明:
- update c=15 成功,insert id = 12阻塞。
加锁的范围如下图
案例八: 普通索引-等值Limit问题
步骤 | 事务A | 事务B |
1 | begin; delete from t where c = 10 limit 2 | - |
2 | - | update t set d = d+ 1 where c = 15; //ok insert into t value(12,12,12) //ok insert into t value(5,5,5) // Duplicate entry '5' for key 'PRIMARY' insert into t value(6,6,6) //blocked |
4 | - | - |
1.根据上面案例8改造,将delete增加limit操作2的操作
2.因为知道了数据加锁值加2条,故在加锁(5,10]之后发现已经有两条数据,故后面不在向后匹配加锁。所以事务B执行成功,加锁范围如下
大家结合上面的分析,尝试理解一下下面的分析吧。
分析一条Sql的加锁情况,參见何登成博文
- Sql: select * from t1 where id=10;
- Sql: delete from t1 where id=10;
在回答这个问题之前我们须要明白几个前提条件: - 前提一:id列是不是主键
- 当前数据库引擎的隔离级别是什么
- Id列不是主键。那么Id列上面有无索引
- Id列上面假设有二阶索引,那么Id是否是Unique Key
- 两个Sql的运行计划是什么?索引扫描?全表扫描?
另外一个Sql即便通过分析结论会使用索引,但实际运行计划有非常多复杂的其它条件,即便”看上去“会走索引可是终于通过运行计划看却走了全表扫描。
组合一:id列是主键。RC隔离级别,运行delete from t1 where id = 10;
create table t1(
id int(32) not null,
name varchar(50) not null,
primary key(id)
);
结论:假设id列是主键,这样的情况仅仅须要在id=10的列上加上X锁。
组合二:id是Unique_key , RC隔离级别,运行delete from t1 where id=10;
create table t1(
id int(32) not null,
name varchar(50) not null,
primary key (`name`),
unique key `key_name`(`name`)
);
运行图(何登成博客获取):
这样的组合以下id是二阶段索引,这样的情况下和组合一加锁不同,DB引擎先走where条件的Id索引,在相应Id索引上id=10的记录上加X锁,然后依据name值回到聚簇索引上面,并对name=d的值加X锁。为什么聚簇索引上面也须要加X锁,假设不加X锁在delete运行的同事假设一个update t1 set id=100 where name=’d’;就会有冲突。
结论:假设id是唯一索引。name为主键,那么会在id索引上面id=10的记录上加X锁。而且name聚簇索引上name=’d’的记录上加X锁。
组合三:id为非唯一索引,RC隔离级别delete from t1 where id=10;
create table t1(
id int(32) not null ,
name varchar(50) not null ,
primary key (`name`),
key `key_name`(`name`)
);
运行图(何登成博客获取):
从图可知,在where条件匹配到的id=10的全部记录均会加上X锁,而且相应到索引上的记录也都会加锁。
结论:若id列上有非唯一索引,那么相应的全部满足SQL查询条件的记录。都会被加锁。
同一时候,这些记录在主键索引上的记录,也会被加锁。
组合四:id列上无索引。RC隔离级别delete from t1 where id=10;
create table t1(
id int(32) not null ,
name varchar(50) not null,
primary key (`name`),
);
运行图(何登成博客获取):
结论:mysql在走where条件的时候因为无法高速通过索引确认影响行,因此会对全部聚簇索引的记录行加上X锁然后返回全部记录。在详细实现时Mysql做了优化,再次通过where条件推断,对于不满足的记录通过unlock_row将X锁进行释放(违背了2PL规范);
组合五:id为主键列。RR隔离级别
这样的情况下加锁机制同组合一一致。
组合六:id列为唯一索引,RR隔离级别
这样的情况下加锁同组合二一致
组合七:id列为非唯一索引,RR隔离级别 (GAP锁)
因为Mysql事务离别为RC的情况下是同意幻读的,可是隔离级别在RR的情况下是不运行幻读。Mysql是怎样做到RR隔离级别不产生幻读?这个组合中会加以说明。
运行图(何登成博客获取):
这里的加锁机制和RC以下的加锁机制相似,唯一差别的是就是RC的加锁情况下添加了一个GAP锁,而且GAP锁不是加到详细的记录上的。而是载入到记录与记录之间的一把锁。
先说说幻读:幻读的意思是说当连续两次运行一个select * from t1 where id=10 for update Sql的时候,前后两次读取的记录数不一致(第二次不会返回比第一次很多其它的记录数)。
RR隔离级别下,因为B+树索引是有序的。那么须要保证的是在id=[6,10)之间不能插入id=10的记录。详细就是在[6,c]与[10,b]之间插入相似[10,aa]或者在[10,b]与[10,d]之间插入[10,c]时都须要有一把锁来使得这些插入不能运行(即:GAP锁)。
GAP锁之所以在组合五和组合六中不会出现的原因是因为上面两种组合保证了记录的唯一性,也就没有必要使用GAP锁。
结论:Repeatable Read隔离级别下,id列上有一个非唯一索引,相应SQL:delete from t1 where id = 10; 首先。通过id索引定位到第一条满足查询条件的记录。加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回。然后读取下一条,反复进行。直至进行到第一条不满足条件的记录[11,f],此时,不须要加记录X锁,可是仍旧须要加GAP锁,最后返回结束。
组合八:id上无索引。RR事务隔离级别
加锁情况(何登成博客获取):
结论:加锁机制和RC隔离级别下相似。差别是同事为每一个记录之间添加了一个GAP锁。不论什么更新/改动/插入等涉及到加锁的Sql语句都无法运行。
欣喜的是同组合四相似,Mysql会提前过滤where条件为不满足条件的提前释放锁。
组合九:Serializable
Serializable情况下。delete from t1 where id=10 通RR情况下一样会通过Gap锁解决掉幻读情况。
Serializable影响的是在select * from t1 where id=10 ,这条Sql在RR 和 RC以下都是快照度不加锁。可是在Serializable情况下会加锁。
一条复杂Sql的分析
Sql用比例如以下(何登成博客获取):
在分析出SQL where条件的构成之后,再来看看这条SQL的加锁情况 (RR隔离级别),例如以下图所看到的:
从图中能够看出,在Repeatable Read隔离级别下,由Index Key所确定的范围,被加上了GAP锁;Index Filter锁给定的条件 (userid = ‘hdc’)何时过滤。视MySQL的版本号而定。在MySQL 5.6版本号之前,不支持Index Condition Pushdown(ICP),因此Index Filter在MySQL Server层过滤。在5.6后支持了Index Condition Pushdown。则在index上过滤。
若不支持ICP,不满足Index Filter的记录。也须要加上记录X锁。若支持ICP,则不满足Index Filter的记录,无需加记录X锁 (图中。用红色箭头标出的X锁,是否要加,视是否支持ICP而定)。而Table Filter相应的过滤条件,则在聚簇索引中读取后,在MySQL Server层面过滤。因此聚簇索引上也须要X锁。
最后。选取出了一条满足条件的记录[8,hdc,d,5,good],可是加锁的数量。要远远大于满足条件的记录数量。
结论:在Repeatable Read隔离级别下,针对一个复杂的SQL,首先须要提取其where条件。Index Key确定的范围,须要加上GAP锁;Index Filter过滤条件,视MySQL版本号是否支持ICP。若支持ICP,则不满足Index Filter的记录。不加X锁,否则须要X锁;Table Filter过滤条件,不管是否满足,都须要加X锁。
死锁的原理和分析
死锁的情况1(何登成博客获取):
死锁情况2(何登成博客获取):
上面的两个死锁用例。第一个非常好理解。也是最常见的死锁,每一个事务运行两条SQL,分别持有了一把锁。然后加还有一把锁。产生死锁。
第二个用例。尽管每一个Session都仅仅有一条语句,仍旧会产生死锁。要分析这个死锁,首先必须用到本文前面提到的MySQL加锁的规则。针对Session 1。从name索引出发,读到的[hdc, 1],[hdc, 6]均满足条件,不仅会加name索引上的记录X锁,而且会加聚簇索引上的记录X锁。加锁顺序为先[1,hdc,100],后[6,hdc,10]。
而Session 2,从pubtime索引出发。[10,6],[100,1]均满足过滤条件。相同也会加聚簇索引上的记录X锁。加锁顺序为[6,hdc,10]。后[1,hdc,100]。
发现没有。跟Session 1的加锁顺序正好相反,假设两个Session恰好都持有了第一把锁,请求加第二把锁,死锁就发生了。
参考资料:
https://www.jianshu.com/p/32904ee07e56
https://www.cnblogs.com/jzssuanfa/p/7394931.html
对其中描述不清晰或者有问题的地方,根据自己的理解修改了一下。
备注:
转载请注明出处:https://blog.csdn.net/WSYW126/article/details/105324239
作者:WSYW126
以上是关于MySQL-加锁规则(间隙锁临键锁行锁表锁)的主要内容,如果未能解决你的问题,请参考以下文章
MYSQL性能调优08_事务及其ACID属性脏读不可重复读幻读隔离级别行锁表锁读锁写锁间隙锁临键锁
MySQL 进阶 锁 -- MySQL锁概述MySQL锁的分类:全局锁(数据备份)表级锁(表共享读锁表独占写锁元数据锁意向锁)行级锁(行锁间隙锁临键锁)
锁--04---锁的分类2----行锁----(记录锁间隙锁临键锁)
MySQL - 全局锁表级锁行级锁元数据锁自增锁意向锁共享锁独占锁记录锁间隙锁临键锁死锁