MySQL锁--04----加锁规则案例

Posted 高高for 循环

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL锁--04----加锁规则案例相关的知识,希望对你有一定的参考价值。

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


加锁规则

环境: 隔离级别—可重复读(RR)

show variables like ‘tx_isolation’;

mysql InnoDB支持三种行锁定方式:

  • 行锁(Record Lock)
    锁直接加在索引记录上面。
  • 间隙锁(Gap Lock)
    锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。
  • 临键锁 ( Next-Key Lock )
    行锁与间隙锁组合起来用就叫做Next-Key Lock。

快照读 和 当前读

InnoDB的默认隔离级别RR(可重复读),在RR下读数据有两种方式:

1. 快照读:-mvcc

普通的 select… 查询都是快照读

mvcc — 基于版本的控制协议

  • 在MVCC下,事物开启执行第一个SELECT语句后会获取一个数据快照,直到事物结束读取到的数据都是一致的

2. 当前读: -next-key lock

读取的数据的最新版本,并且在读的时候不允许其它事物修改当前记录

临键锁(Next-Key Locks),也就是结合gap锁与行锁,

实现:

  1. 快照读(snapshot read)
    简单的select操作(不包括 select … lock in share mode, select … for update)

  2. 当前读(current read)
    select … lock in share mode、select … for update
    insert、update、delete

加锁规则(RR隔离级别下)

1.原则

  • 原则1:加锁的基本单位是next-key lock。(是一个前开后闭的区间)
  • 原则2:查找过程中访问到的对象才加锁

2.优化

  • 优化1:唯一索引上的等值查询,匹配上时,给唯一索引加锁,next-key-lock会退化为行锁。

  • 优化2:索引上的等值查询,向右遍历且最后一个值不满足等值条件的时候,next-key-lock退化为 间隙锁 Gap Locks 。

  • 优化3:唯一索引和普通索引在范围查询的时候 都会访问到不满足条件的第一个值为止

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的加锁内容是非主键索引树上符合条件的索引项,以及这些索引项对应的主键索引树上相应的索引项。在两个索引上都加了锁

MySQL死锁处理

案例综合

数据

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);

表结构

  • 以上数据为了解决幻读问题,更新的时候不只是对上述的五条数据增加行锁
  • 还对于中间的取值范围增加了6个临键锁,(-∞,5](5,10](10,15](15,20](20,25](25,+supernum](其中supernum是数据库维护的最大的值。为了保证临键锁都是左开右闭原则。)

案例一:间隙锁简单案例


优化2:索引上的等值查询,向右遍历且最后一个值不满足等值条件的时候,next-key-lock退化为 间隙锁 Gap Locks 。

当有如下事务A和事务B时,事务A会对数据库表增加(10,15]这个区间锁,经过优化2,退化为间隙锁(10,15)。

  • 这时insert id = 12 的数据的时候就会因为区间锁(10,15)而被锁住无法执行。
  • 这时insert id = 10 和 insert id = 15的数据是未被锁定,报错:Duplicate entry ‘15’ for key ‘PRIMARY’

案例二:间隙锁死锁问题



不同于写锁相互之间是互斥的原则,间隙锁之间不是互斥的。

如果一个事务A获取到了(5,10)之间的间隙锁,另一个事务B也可以获取到(5,10)之间的间隙锁。这时就可能会发生死锁问题。

如下案例:

  • 事务A获取到(5,10)之间的间隙锁,在事务提交,间隙锁释放之前,是不允许其他的DDL操作
  • 事务B也获取到了间隙锁(5,10),这时两个事务就处于死锁状态

案例三: 等值查询—唯一索引

优化2:索引上的等值查询,向右遍历且最后一个值不满足等值条件的时候,next-key-lock退化为 间隙锁 Gap Locks 。

  1. 加锁的范围是(5,10]的临建锁
  2. 由于数据是等值查询,并且表中最后数据id = 10 不满足id= 7的查询要求,故id=10 的行级锁退化为间隙锁,(5,10)

执行说明:

  • 1.事务B中id=8会被锁住,而insert id=10 和 update id=10的时候不都会被锁住。

案例四: 等值查询—普通索引


  1. 初步加锁,加锁的范围是(0,5],(5,10]的临建锁。

  2. 由于c是普通索引,根据优化2,搜索到5后继续向后遍历直到搜索到10才放弃,故加锁范围为(5,10]

  3. 根据优化2,由于查询是等值查询,并且最后一个值不满足查询要求,故间隙锁退化为(5,10)

  4. 最终的加锁范围是:(0,10)

执行说明:

  1. 因为加锁是对普通索引c加锁,而且因为索引覆盖,没有对主键进行加锁,所以事务B执行正常。
  2. 因为加锁范围(0,10),故insert id=4 和 update id=7执行阻塞。
  3. 需要注意的是,lock in share mode 因为覆盖索引故没有锁主键索引,如果使用for update 程序会觉得之后会执行更新操作故会将主键索引一同锁住。

案例五: 范围查询—唯一索引


  1. next-key lock 增加范围锁(5,10]
  2. 根据原则5,唯一索引的范围查询会到第一个不符合的值位置,故增加(10,15]
  3. 因为等值查询有id =10,根据原则3间隙锁升级为行锁,故剩余锁[10,15]
  4. 因为查询并不是等值查询,故[10,15]不会退化成[10,15)
  5. 最终的加锁范围是:[10,15]

执行说明:

  1. insert id=8 和 update id=16,执行OK。

  2. update id=15、insert id=10 和 insert id=13 和insert id=15, 执行阻塞。

案例六: 范围查询—普通索引

  1. next-key lock 增加范围锁(5,10],(10,15]
  2. 因为c是非唯一索引,故(5,10]不会退化为10
  3. 因为查询并不是等值查询,故[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的就有两条记录


  1. next-key lock 增加范围锁(5,10],(10,15]
  2. 因为是等值查询故退化为(5,10],(10,15)

执行说明:

  1. update c=15 成功,insert id = 12阻塞。

加锁的范围如下图

案例八: 普通索引-等值Limit问题

  1. 根据上面案例8改造,将delete增加limit操作2的操作
  2. 因为知道了数据加锁值加2条,故在加锁(5,10]之后发现已经有两条数据,故后面不在向后匹配加锁。所以事务B执行成功,加锁范围如下

Sql的加锁分析----图解

分析一条Sql的加锁情况,參见何登成博文

  • select * from t1 where id=10;
  • delete from t1 where id=10;

在回答这个问题之前我们须要明白几个前提条件:

前提条件:

  1. id列是不是主键
  2. 当前数据库引擎的隔离级别是什么
  3. Id列不是主键。那么Id列上面有无索引
  4. Id列上面假设有二阶索引,那么Id是否是Unique Key
  5. 两个Sql的运行计划是什么?索引扫描?全表扫描?
  6. 另外一个Sql即便通过分析结论会使用索引,但实际运行计划有非常多复杂的其它条件,即便”看上去“会走索引可是终于通过运行计划看却走了全表扫描。

组合一:id列是主键。RC隔离级别

运行delete from t1 where id = 10;

create table t1(
 
    id int(32) not null,
 
    name varchar(50) not nullprimary 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 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锁。

以上是关于MySQL锁--04----加锁规则案例的主要内容,如果未能解决你的问题,请参考以下文章

Day885.NextKeyLock加锁规则 -MySQL实战

MySQL-加锁规则(间隙锁临键锁行锁表锁)

MySQL-加锁规则(间隙锁临键锁行锁表锁)

Day894.加锁规则的一些问题 -MySQL实战

聊聊MySQL的加锁规则《死磕MySQL系列 十五》

mysql 中的 latch锁和Tlock(事务锁), DML加锁规则,以及死锁分析。