MySQL在InnoDB存储引擎下的加锁规则探讨(一) - Go语言中文社区

MySQL在InnoDB存储引擎下的加锁规则探讨(一)


本文是在看了何登成大神的技术博客后做的总结,部分图片来自其技术博客,主要是方便自己日后回顾,在此感谢原博客作者提供的经典之作,原作者博客地址:http://hedengcheng.com/?p=771#_Toc374698322

一、什么是快照读与当前读

MVCC(多版本并发控制协议)在InnoDB存储引擎中主要提供两种读方式,一种是快照读,一种是当前读。

快照读:

所谓快照读就是一致性非锁定读,就是读取的是历史版本,不用加锁,提高并发,在数据库不同的隔离级别下读取的历史版本有所不同,在RR(REPEATABLE READ)隔离级别下读的历史版本总是事务刚开始的那一个版本,所以可以避免不可重复读与幻读,因为其对其他事务提交的数据是不可见的。在RC(READ COMMITTED)隔离级别下读的是最新的历史版本,或者是可见版本(另一个事务刚已经提交的数据),这样就会出现不可重复读现象。

SELECT * FROM table WHERE ........

当前读:

当前读时一致性锁定读,采取加锁的方法保证并发数据的一致性:

SELECT * FROM table WHERE ...  LOCK IN SHARE MODE(加的是共享锁(S))

SELECT * FROM table WHERE ...  FOR UPDATE(加的是排他锁(X))

INSERT INTO table ........ (在插入数据前先读取数据,进行唯一性检查,读取数据时会加X锁)

UPDATE table SET .........  (在更新数据之前得先读取数据,在对读取的数据进行修改,读取数据时会加X锁)

DELETE FROM table ........(在删除数据之前也是得先读取数据,读取数据时会对数据加X锁)

说明:在serializable隔离级别下,MVCC退化成了base-lock concurrency control(基于锁的并发控制),也就不存在快照读了,全是当前读,序列化的操作,并发性受到了很大的影响,一般不采用该隔离级别。

二、加锁分析的前提

对于sql加锁的分析,是有一些前提条件的,不能笼统的说加没加锁,加了什么锁。得看过滤条件是不是主键,如果不是主键,有没有索引,索引是否是唯一索引,当前数据库的隔离级别是什么,该sql语句的执行计划是什么(全表扫还是索引扫描)。以上这些就是所说的分析加锁的前提条件,在分析加锁时,得先判断是这些前提条件的那种组合。

对于快照读,也就是不加锁的方式,就不予分析了,对于当前读,选取delete from t1 where id = 10语句对九种组合进行加锁分析:

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

组合二:id是唯一索引,RC隔离级别

组合三:id是普通索引,非唯一,RC隔离级别

组合四:id没有索引,RC隔离级别

组合五:id是主键,RR隔离级别

组合六:id是唯一索引,RR隔离级别

组合七:id是普通索引,非唯一,RR隔离级别

组合八:id没有索引,RR隔离级别

组合九:serializable隔离级别

三、实例分析

1、组合一:id列是主键,RC隔离级别

表t1:

sql语句:DELETE FROM t1 WHERE id = 10

加锁情况见下图:

测试:

上图中指出了在这种组合下面会对相应的主键记录进行加锁,加的是record lock。

模拟事务A删除id为10的记录,未提交,事务B查询(当前读)id为10的记录,出现锁阻塞:

从上面可见对主键id进行了加锁处理。

 

2、组合二:id是唯一索引,RC隔离级别

表t1:

sql语句:DELETE FROM t1 WHERE id = 10

加锁情况见下图:

测试:

上面图中指出了会加两个X锁,一个是对于唯一索引id索引出的记录加锁,另一个是通过此唯一索引查出的聚集索引记录上加锁,防止另一个事务通过聚集索引对该条记录进行操作。

模拟事务A删除id为10的记录,未提交,事务B查询(当前读)id为10的记录,出现锁阻塞:

从上面可以看出对id索引进行了加锁处理。

上面id为10的记录对应的主键是d,模拟事务B当前读主键为d的记录:

从上面可以看出同时对相应记录上的主键进行了加锁。

 

3、组合三:id是普通索引,非唯一,RC隔离级别

表t1:

sql语句:DELETE FROM t1 WHERE id = 10

加锁情况见下图:

测试:

从上面可以看出,普通索引与唯一索引加锁的情况本质是一样的,只是唯一索引确定记录唯一,而普通索引可能会存在重复记录,而对于重复的记录每个都需要加锁处理。

模拟事务A删除id为10的记录,未提交,事务B查询(当前读)id为10的记录,出现锁阻塞:

从上面可以看出这里是对索引id进行的加锁处理,加的是record lock

上面id为10的记录对应的主键是b与d,模拟事务B当前读主键为b与d的记录,都出现阻塞:

上图看出对主键为b的记录进行了加锁

上图对主键为d的记录进行了加锁。

 

4、组合四:id没有索引,RC隔离级别

表t1:

sql语句:DELETE FROM t1 WHERE id = 10

加锁情况见下图:

由于id列上没有索引,因此只能走聚簇索引,进行全部扫描。从图中可以看到,满足删除条件的记录有两条,但是,聚簇索引上所有的记录,都被加上了X锁。无论记录是否满足条件,全部被加上X锁。既不是加表锁,也不是在满足条件的记录上加行锁。

有人可能会问?为什么不是只在满足条件的记录上加锁呢?这是由于MySQL的实现决定的。如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行过滤。因此也就把所有的记录,都锁上了。

注:在实际的实现中,MySQL有一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。

结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。

测试一:

模拟事务A删除id为10的记录,未提交,事务B查询(当前读)id为10的记录,出现锁阻塞:

从上面可以看到此时阻塞发生在主键为d的记录上,我们查询的是id为10的记录,满足的记录有两个,一个主键是d,一个主键是g。根据聚簇索引的有序性,主键为d的记录先被检索到,锁阻塞发生在了先被检索到的记录上。

测试二:

上面那个测试只能看出在id为10的主键上进行了加锁,但是看不出前文所说的在这种组合下对所有记录进行加锁和放锁的过程,下面这个测试可以说明这个问题:

事务B当前读id为5的记录:

在表中id为5的记录对应的主键是a,上面的锁阻塞信息中可以看到锁阻塞发生在了主键为d的记录上,有人可能会感到很奇怪,其实这正好印证了上文中说的在无索引进行快速过滤的情况下,InnoDB会将所有记录一条条返回给mysql server,一条条进行加锁,再进行全表扫描过滤,将不满足条件的记录进行放锁操作。事务A的delete操作在经过上锁放锁操作后,在主键为d与主键为g的记录上还保留着X锁,其他记录上的锁已经释放,而此时事务B当前读id为5的记录,此时也需要全表扫描,在没条记录上加上S锁,一直到主键为d的记录,新的S锁与旧的的X锁不兼容,导致锁阻塞,这印证了需要逐条记录加锁的过程,另一方面,锁阻塞没有发生在一开始的主键为a的记录上,也侧面说明了delete操作对不满足条件的记录(比如a)最后进行了放锁操作。

测试三:

这个测试直接对主键a进行查询,由于此时不需要进行全表扫描,可以通过索引快速过滤记录,并且主键为a的记录上的锁已经释放,那么该测试不会出现锁阻塞,可以直接获取当前读的结果:

 

5、组合五:id是主键,RR隔离级别

组合五,id列是主键列,Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条SQL,加锁与组合一i是主键,READ COMMITTED隔离级别一致。不在重复分析。

 

6、组合六:id是唯一索引,RR隔离级别

组合六的加锁,与组合二:id是唯一索引,READ COMMITTED隔离级别一致。两个X锁,id唯一索引满足条件的记录上一个,对应的聚簇索引上的记录一个。不再重复分析。

 

7、组合七:id是普通索引,非唯一,RR隔离级别

表t1:

sql语句:DELETE FROM t1 WHERE id = 10

加锁情况见下图:

MySQL的InnoDB引擎在设计上比较奇葩,按照数据库理论,在RC的隔离级别下会出现不可重复读,在RR的隔离级别下避免不可重复读,但是允许出现幻读,但是InnoDB引擎下在RR隔离级别由于gap锁的存在不仅可重复读,还能避免幻读,这点和其他大不相同,因为在查询的范围内进行了gap锁,所以查询范围内的记录是不能被改变的,也不能在此范围内插入新的记录,那么在此查询得到的记录本身不会发生变化,查询到的记录条数也不会发生变化,即可重复读并且避免幻读。

前面RR隔离级别下的组合组合五、组合六里面都只加了record lock,并没有加gap锁,这又是为什么呢?这样会引起幻读吗?其实这个也很好解释,组合五强调主键、组合六强调唯一索引,这两个都是唯一性约束,也就是说表中不能再插入相同的记录,当前读出来的记录永远只会是唯一的一条,也就不需要gap锁来避免新的相同的记录的插入。

其实,针对此问题,还有一个更深入的问题:如果组合五、组合六下,针对SQL:select * from t1 where id = 10 for update; 第一次查询,没有找到满足查询条件的记录,那么GAP锁是否还能够省略?这点很好验证,在这种情况下是存在gap锁的,在查询的空记录的相邻两个记录之间加入gap锁,这样就可以避免新插入当前查询为空的记录,因为新插入的该记录一定只能在这相邻的两个记录之间,而此时这个区间已经加了gap锁。其实由这一点也可以知道,mysql的加锁分析只要本着数据库中各种隔离级别下的一些特性,是很好分析出来的,就算遇到陌生的场景,要学习的是这种分析方法。

结论:Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。

测试一:

测试普通索引上的record锁:

模拟事务A删除id为10的记录,未提交,事务B查询(当前读)id为10的记录,出现锁阻塞:

上面锁阻塞发生在索引10上。

测试二:

测试普通索引id上的gap锁:

事务B插入记录('s',7),根据索引B+树组织的有序性,7在索引6与10之间,这中间会有一个gap锁,所以该语句会阻塞:

上面可以看到lock_mode中出现了GAP。

测试三:

测试主键上的record lock:

事务B当前读主键name为b的记录,由于在对id为10的索引加锁时也会对其主键加锁,即主键b、d对应的记录都已经加了X锁,此时会出现锁阻塞:

 

8、组合八:id没有索引,RR隔离级别

表t1:

sql语句:DELETE FROM t1 WHERE id = 10

加锁情况见下图:

如图,这是一个很恐怖的现象。首先,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有6条记录,一共需要6个记录锁,7个GAP锁。试想,如果表上有1000万条记录呢?

在这种情况下,这个表上,除了不加锁的快照度,其他任何加锁的并发SQL,均不能执行,不能更新,不能删除,不能插入,全表被锁死。

当然,跟组合四:id无索引、READ COMMITTED隔离级别类似,这个情况下,MySQL也做了一些优化,就是所谓的semi-consistent read。semi-consistent read开启的情况下,对于不满足查询条件的记录,MySQL会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了innodb_locks_unsafe_for_binlog参数为on。

结论:在Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用。

测试一:
测试主键上的record lock

模拟事务A删除id为10的记录,未提交,事务B查询(当前读)id为10的记录,出现锁阻塞:

上面阻塞的为什么是主键为a的记录,与组合四中的分析是同理的,这里不再阐述。只是这里不进行放锁的过程。

测试二:

对记录间的gap锁测试:

在主键b与d之间插入c,由于gap锁的存在,会出现锁阻塞

上面出现了GAP锁。

 

9、组合九:serializable隔离级别

针对前面提到的简单的SQL,最后一个情况:Serializable隔离级别。对于SQL2:delete from t1 where id = 10; 来说,Serializable隔离级别与Repeatable Read隔离级别完全一致,因此不做介绍。

Serializable隔离级别,影响的是SQL1:select * from t1 where id = 10; 这条SQL,在RC,RR隔离级别下,都是快照读,不加锁。但是在Serializable隔离级别,SQL1会加读锁,也就是说快照读不复存在,MVCC并发控制降级为Lock-Based CC。

结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于所有的情况,而是隔离级别相关的。Serializable隔离级别,读不加锁就不再成立,所有的读操作,都是当前读。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/Wenlong_L/article/details/84133753
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-07 19:54:09
  • 阅读 ( 918 )
  • 分类:数据库

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢