导读
最近在学习查找MySQL中"锁"的相关资料时,发现网上各种言论观点杂乱不堪且版本混乱,很容易让人深陷其中、很是蒙圈。笔者认真研读了MySQL8.0官方指导手册,并广泛搜集各家观点,整理了一份参考性较强的关于MySQL中"锁"机制的知识点合集,以供参考学习。
注:本文所有内容面向MySQL8.0版本,部分条目不适用于MySQL5.X。
基础概念篇
01 怎么认识"锁"
02 "锁"的分类
03 加"锁"过程
04 给谁加"锁"
05 加"锁"目的
07 加"锁"对象
08 "锁"和事务
1## 开启事务2种方法
2-- 一种是显式开启事务
3START TRANSACTION / BEGIN
4-- 另一种是关闭自动提交
5SET autocommit = 0
6
7## 结束事务
8COMMIT / ROLLBACK
1select ……;
2等价于
3START TRANSACTION;
4selece ……;
5COMMIT;
09 "读象"
read phenomena,官方文档给出的英文写法,未找到相关权威翻译名词。特指MySQL读取过程中存在的副作用,例如脏读、幻读等
需要指出:MySQL依靠MVCC的快照机制,某种程度上RR隔离级别已经避免了幻读,但仍可触发,官方文档也给予相应的说明。具体请阅读后面的实战案例。
10 快照读和当前读
实战案例篇
以下所有案例均依托Navicat Primium12工具。初始建表语句:
1create table test(id int, name varchar(20), primary key(id));
2insert into test values(1, 'A');
3insert into test values(3, 'C');
11 3种"读象"
脏读、不可重复读和幻读应该是困扰很多人的一个常见概念问题,尤其是后两者的区别,这里通过几个案例进行阐释说明。
首先来看官方文档给出的定义:
An operation that retrieves unreliable data, data that was updated by another transaction but not yet committed. It is only possible with the isolation level known as read uncommitted.
大意:某个操作中处理了由其他事务更新但尚未提交的数据,这个数据是不可靠的数据,仅发生于RU隔离级别。
案例:
RU存在脏读:事务A读到了事务B更改但未提交的数据
官方文档给出的定义:
The situation when a query retrieves data, and a later query within the same transaction retrieves what should be the same data, but the queries return different results (changed by another transaction committing in the meantime).
大意:在一项事务查询数据期间,由于其他事务同时进行了提交,造成其前后两次查询到的数据结果不一致。
案例:
RC避免了脏读,但存在不可重复读
A row that appears in the result set of a query, but not in the result set of an earlier query. For example, if a query is run twice within a transaction, and in the meantime, another transaction commits after inserting a new row or updating a row so that it matches the WHERE clause of the query.
大意:之前查询的结果中不存在、但之后查询得到的记录称作是幻读。例如,一个查询执行两次,期间另一个事务进行了插入或更新记录并提交,导致前一个事务两次查询结果不一致。
个人观点,幻读本身当然属于不可重复读的一种,毕竟两次读取结果"不一致"。但幻读侧重的是之前没有、之后虚幻出来了新行这种特定操作。
案例:
①,RR级别可避免RC级别中的不可重复读问题:
RR不存在不可重复读数据
②,特殊情况下仍可触发幻读
RR级别下,特殊操作仍可触发幻读(更新快照)
实际上,MVCC机制只是为保证读取结果采取快照的方式,所以能保证可重复读,但对于执行insert、update和delete操作时,仍然会实际检测当前数据库中最新的记录状态:当其他事务提交的最新数据与本事务中的增删改操作符合条件时,仍然会有影响。
这点不难理解,毕竟要保证数据库的状态一致性,但值得诧异的是经过update之后,居然会更新事务中的快照版本。例如图中所示案例,初次查询有2条记录,update时实际更新的是3条,但再次查询时结果也更新成了3条。而且,更重要的是,这种现象并不具有普遍性:仅当事务执行update操作时才会更新快照版本,而对于delete和insert操作则是只检测状态不更新快照版本。
事务的insert操作不会更新快照版本
更一般的,进一步测试了事务B执行的其他增删改操作对事务A是否更新快照版本的影响,两两组合,得到如下试验结论:
如上幻读仅发生在其他事务插入新记录且提交后,本事务更新数据后的再次查询中
当然,官方文档对此给出了注解:
大意是说:快照读(snapshot)仅适用于查询语句,对DML(数据操纵语言,即增删改操作)不适用。其他事务执行删除或更新操作并提交,当前事务虽然"看不到"这些更改,但在执行自己执行更新或删除操作后对其可见。虽然此注解足以解释上述案例结论,但笔者实际上仍然存在前述表中的疑问。
最后需要指出的是,MVCC机制是基于快照版本的并发控制,与之对应的是LBCC,当采用LBCC读取数据时,则总能读到最新的数据。当然,这与RR隔离级别和MVCC机制并不矛盾。
加锁读总是读取最新结果,但不影响快照版本
12 快照版本
MVCC是基于多版本的并发控制,查询结果以快照版本为准。但不同隔离级别的快照版本采集原则不一致。在RR隔离级别中,通过MVCC机制实现了在同一事务中的可重复读取问题,而且该快照是在首次查询时采集的版本号信息,而与开启事务时机无关。
RR级别中首次查询建立快照版本
而且,RR级别中一旦建立了快照版本,则在该事务的后续查询中均采用该快照版本作为结果(当然,通过前面的案例发现也有例外);与之对应的是,RC级别中,每次查询都采集最新的快照版本作为结果,所以自然也就存在不可重复读的问题。
13 加锁类型
首先简单介绍记录锁、间隙锁和临键锁:
记录锁根据索引锁定相应记录,即使相应的表中不建立任何索引时。实际上所有InnoDB表都存在索引,当用户建表时未显式设置索引时,引擎会自动建立隐藏索引,这也是InnoDB底层基于聚簇索引存取整条记录的特性使然。
记录锁仅对索引满足查询条件的记录加锁
如果说记录锁是对命中的记录进行加锁,那么间隙锁是则是对查询区间范围内但是不存在的记录进行预订加锁,例如下图中假设表中不存在id=2、3的记录,但因为满足查询范围,所以会对其加间隙锁。
间隙锁对满足查询条件的记录间隙加锁
显然,间隙锁是以牺牲一定并发性能为代价换取高一致性。实际上,这也是所有锁在做的一件事,即在一致性和并发能力之间获得某种均衡。
需要指出的是:
在记录锁和间隙锁的基础上,临键锁=记录锁+间隙锁。
临键锁=记录锁+间隙锁
RC隔离级别中只有记录锁,而没有间隙锁和临键锁;RR级别中如果是等值查询则是记录锁,范围查询则是临键锁(即记录锁+间隙锁),在5.6以前版本中可以通过全局参数设置是否开启,但在8.0版本已移除该变量。
14 索引类型对加锁影响
在明确加锁类型后,还需考虑不同索引对加锁的影响。首先指出,在InnoDB引擎下即使创建表时不显式指定索引,引擎也会自动生成隐藏索引用于聚簇存储记录数据。基于此,索引对加锁的影响有如下几种情况(引自官方文档):
不同类型下的加锁分析详见文末参考资料2中文档,讲解充分,受到广泛转发引用,这里个人就不班门弄斧了。
15 锁竞争和死锁
一般来说,锁具有排他性。如果是共享锁(S锁),可以和另一个共享锁(S锁)同时拥有,但无法和一个排他锁(X锁)同时拥有;而对于一个X锁,则无法跟任何其他锁并发。当多个事务企图同时占用某一资源需要加锁时,就有可能发生锁竞争甚至死锁。
多个事务竞争同一资源
在上述案例中,三个事务依次请求对数据表加X锁,其中事务A成功请求,事务B和事务C会处于等待。当事务A提交事务后,虽然事务B和事务C处于同时竞争加锁状态,但由于MySQL对事务调度的FIFO(First In First Out,先入先出)特性,二者不会发生死锁,而是优先满足事务B加锁请求,待事务B提交事务后再满足事务C的加锁请求。
①,锁竞争+索引重复冲突造成死锁:
三个事务竞争资源存在索引重复
这个案例与锁竞争中的例子类似但又不同:假设事务A、事务B和事务C同时请求插入一条数据(插入语句都是加X锁),此时不仅仅是因为加锁冲突,还存在索引重复的问题,此时一旦事务A回滚释放锁后,事务B和事务C则会陷入死锁。这是一种特殊的死锁触发原因。
②,竞争同一资源出现死循环:
两个事务先竞争,后死锁
在这个案例中,先是事务A和事务B分别对id=1和id=2的记录加X锁,然后事务A继续对id=2的记录请求加锁时,因为该记录已被事务B占有,所以事务A只能等待;但此时事务B又企图对事务A已经占有的id=1记录加X锁,造成事务A和事务B在各自占有一定资源的基础上分别企图占用对方已加锁的资源,逻辑上冲突,骑虎难下,引擎不可能通过时间调度得以解决,故而发生死锁。
发生死锁后,引擎会根据相关的事务间的重要程度(包括占用资源多少、时间先后等)来选择一个进行回滚:例如上例中,事务A先于事务B请求加X锁,可将事务B看成是直接造成死锁的原因,所以选择对B进行回滚,而允许A加锁成功。
如果能看到这里,相信应该已对MySQL中锁机制有较为全面的了解,那就赏个转发或者在看吧!
参考资料:
1. MySQL8.0官方文档(公众号:小数志 后台回复"教程"提供下载)
2. 何登成的技术博客:http://hedengcheng.com/?p=771#_Toc374698323