MySQL的并发处理机制,有MVCC及锁机制来处理,上篇简要说明了 MVCC及隔离级别:mysql的并发处理机制_上篇 ,这篇来说说mysql下的锁。

1 Innodb的锁
在innodb中,有4种类型的锁:IX、X、IS及S锁,其说明如下:
类型 | 说明 | 场景 |
S | 共享锁 | 针对于RS隔离级别的查询或者添加Lock in share mode的SELECT查询而产生的锁 |
X | 排它锁 | 针对于update、delete、insert操作而产生的锁 |
IS | 意向共享锁 | 表级别的锁,在添加S锁之前对表格添加IS锁 |
IX | 意向排他锁 | 表级别的锁,在添加X锁之前对表格添加IX锁 |
1.1 锁定兼容情况
四个锁之间的兼容性,需要分成两种情况来讨论,锁粒度小于表级别的锁的兼容情况,表级的锁兼容情况。
- 锁粒度小于表级别的锁的兼容情况
-
mysql的并发处理机制_下篇 - 对于这两行锁的兼容说明如下:
- 假设有一行数据,添加了行锁S锁,那么这个行数据,可以提供给其他事务进行S锁的申请和添加,但是不支持其他事务对这一行进行X锁的申请和添加。比如,事务A,对 pk100 这一行进行了 查询操作并添加了S锁,那么其他事务仍然可以对这一行数据进行查询,但是不能对这行数据进行 UPDATE 跟 DELETE 操作,会处于锁等待情况,直到该事务A结束并释放S锁;
- 假设有一行数据,添加了X锁,那么这个行数据,不允许其他事务对这一行数据进行加锁。比如,事务A,对pk100这一行进行了UPDATE操作,那么其他事务在事务A没有结束之前,都无法对这一行数据申请 S锁。
-
mysql的并发处理机制_下篇 - 对于表级别的锁兼容性如下:
- 当一个表格持有S表锁时,不需要其他事务对该表格申请X锁跟IX锁,但是允许申请S跟IS锁。比如,事务A对表格tba全表读,加了S表锁,期间支持其他事务对tba全表读(申请S表锁成功)、支持其他事务对tba行数据查询(申请IS表锁成功),但是不支持对表格全表的修改操作(申请X表锁等待)跟不支持对表格行数据修改操作(申请IX表锁等待);
- 当一个表格持有X表锁时,持有锁期间,不支持其他所有锁的申请;
- 当一个表格持有IS表锁时,允许申请 S表锁、IS表锁、IX表锁,但是不支持X表锁申请。
- 比如,事务A对表格tba 查询了 id = 10(id为主键)这一行数据,这个时候,表格tba持有IS表锁,id = 10 这一行持有 S 行锁,期间,支持其他事务对 tba 全表查询(申请表锁S成功)或者 基于索引查找(申请表锁IS成功)
- 如果需要对行 id = 20 进行数据修改,则会先申请 tba 的表锁 IX(申请成功),然后再申请id=20行锁X (申请成功);如果需要对 id = 10 这一行数据进行修改,则会申请 tba的表锁 IX(表锁申请成功),然后申请 id = 10 的行锁X(申请堵塞,因为 id = 10 正持有S锁);
- 如果需要对表格进行全表修改,需要申请表锁(X锁),这个时候,IS锁的优势来了,当查看表格是否有其他事务在访问操作时,一看表锁IS就知道有其他事务对表格内部某些数据持有S锁,并且还没有释放,那么这个时候,申请X锁就会处于等待状态,而不需要一行一行去查询每一行数据有没有被其他事务持有锁,可以大规模的减少查询 锁申请情况;
- 当一个表格持有IX表锁时,支持申请IS、IX表锁,但是不兼容S、X表锁。
- 比如,事务A对表格 tba 中 id=10 (id为主键)进行进行 数据修改,这个时候,会对表格 tba 先申请一个 IX 表锁(申请成功),然后申请 id =10 的 X 行锁,申请成功,则 事务A 持有 IX 表锁、id=10的X 行锁,此时事务B 查询 id=20的行,申请表锁 IS 成功,申请 id=20的 S 行锁成功;事务C 修改 id=30的行数据,申请表锁 IX 成功,申请 id=30的行锁 X成功;但是,事务D中,对整个表格发起update或者全表SELECT操作,需要申请 X表锁或者S表锁,正常情况下,应该要对表格的每一行数据进行查看,确保每一行数据的行锁情况,但是因为有了意向锁,事务D一看到 tba 持有 了IX锁,则明白,tba 中某些行持有X锁,则会不兼容其他事务对tba 表锁S ,表锁X的申请。
- 为什么要引入意向表锁?
- 在没有意向锁的时候,如果事务T 需要给表格 A 添加 一个S 表锁,那么就意味这这个表格内部的每一行数据,都不能有X锁,才能够申请 S 表锁成功,如果表格数据很多,一行行查找非常浪费加锁时间,这个时候,就出现了表格意向锁,当表格内部某些行发生 UPATE DELETE INSERT操作,则会对表格 加上 一个意向 IX 表锁,这样 事务T在申请 表格A的 S 表锁时,只需要检查 表格 A 是否有 IX表锁,如果有,则意味内部有 部分行数据持有X锁,则直接进入等待情况,如果表格没有 IX表锁,则直接申请S表锁成功,这是一个多么节约加锁时间的操作!
1.2 锁的级别
- Table Lock
- 表锁,如果没有where条件、无可用索引或者获取的行记录过多,则会使用 table full scan,添加表锁
- Record Lock
- 记录锁,如果执行计划使用了索引,则会根据索引的查找情况添加行锁
- Gap Lock
- 在RR、RS隔离级别,发生在索引值之间,在连续的两个索引值之间添加锁,加锁后,这两个索引值之间,无法插入新的索引值,不包含行记录
- Next-Key Lock
- Record Lock 跟Gap Lock的组合,合体成为Next-KEY Lock
表锁、行锁都相对好理解,这里尝试简单说明下 GAP LOCK。
假设当前隔离级别为RR,表格 tbgap( id int auto_increment primary key not null , name varchar(50) , sort int , key ix_sort (sort)) engine=innodb;
表格数据如下:
在索引ix_sort上,一共有7个间隙,分别为(-∞,(1,6)),((1,6),(2,5)),((2,5),(3,2)),((3,2),(5,4)),((5,4),(6,1)),((6,1),(7,3)),((7,3),+∞),而根据实际的隔离级别及锁申请情况,加在这些间隙上的锁,则成为 GAP LOCK 。
1.3 锁与隔离级别(不考虑 lock in shar mode跟for update )
- RU,读未提交记录,不加锁读,正常写锁;
- RC,快照读,无锁;当前读,加 Record Lock
- RR,快照读,无锁;当前读,对读取到的记录加 Record Lock,同时为了确保where条件范围内的数据无变化,会增加Next key lock
- RS,读写均为当前读,不支持快照读。包括select 在内,对读取到的记录加 Record Lock,同时为了确保where条件范围内的数据无变化,会增加Next key lock。
2 锁的申请与释放过程
看SQL语句的锁情况,需要结合隔离级别、执行计划、表结构等,同一个SQL,不同的隔离级别、表结构、执行计划,其锁情况不一定是一样的!
本次模拟这3个表格,age列分别:无索引、有一般索引、有唯一索引。表结构结束及数据如下:
CREATE TABLE tb_no_index ( id int primary key not null auto_increment, age int not null, name varchar(100) );
CREATE TABLE tb_index ( id int primary key not null auto_increment, age int not null, name varchar(100) KEY ix_age(age) );
CREATE TABLE tb_unique_index ( id int primary key not null auto_increment, age int not null,name varchar(100) UNIQUE KEY ix_age(age) );
INSERT INTO tb_no_index(age) values(2),(9),(21),(4),(7),(25);
INSERT INTO tb_index(age) values(2),(9),(21),(4),(7),(25);
INSERT INTO tb_unique_index(age) values(2),(9),(21),(4),(7),(25);
每个表格IX_age的索引行数就据如下图展示:
age | 2 | 4 | 7 | 9 | 21 | 25 |
id | 1 | 5 | 3 | 6 |
每个表格主键上面的行数就据如下图展示:
name | null |
2.1 Read Uncommitted
所有事务隔离级别设置: set session transaction isolation level read Uncommitted ;
RU是读未提交,不添加 LOCK IN SHARE MODE 跟 FOR UPDATE 的 SELECT 语句,均为读未提交,不加锁,存在脏读、不可重复读及幻读。
所有UPDATE、DELETE、INSERT获取当前读记录,加锁。
表格 SQL | select * from tbname where age/id ... | update tbname set name=... where id = 4 | where age = 21 | where age between 5 and 15 |
tb_no_index | 读不加锁,读未提交数据 可能有脏读、不可重复读及幻读 | 当前读,根据主键修改数据 tbname 加意向表锁 IX id=4 加 行锁 X | 表格的age列无索引,所以update过程中,全表加X锁 支持semi-constent-read,如果有其他update语句修改其他行不堵塞,但是不支持 select ... for update | 同左 |
tb_index | 表格的age列有索引,update过程中 tb_index 加 表格意向锁 IX age索引上面,age=21 行添加行锁 X 再在主键上,给id=3 这一行数据,添加行数 X | 表格的age列有索引,update过程涉及age=7,9 两行数据 tb_index 加表格意向锁 IX age索引上面,age=7,age=9 行添加行锁 X 再在主键上,给id=2,id=5 这一行数据,添加行数 X | ||
tb_unique_index | 同上 |
2.2 Read Committed
所有事务隔离级别设置: set session transaction isolation level read committed ;
RC是读已提交,不添加 LOCK IN SHARE MODE 跟 FOR UPDATE 的 SELECT 语句,均为 快照读,不加锁,同个事务内读取同一个版本的数据,可能非最新数据,但是不存在脏读、不可重复读及幻读情况。
下表中,黄绿色 字体 是RC与RU隔离级别不同的地方,仔细阅读分析结果可以知道,在 RU 跟 RC 间,最大的区别在于 SELECT 的查询模式,RU 为 读未提交,而 RC 为快照读。UPATE/DELETE/INSERT的加锁模式类同。
表格 SQL | ||||
快照读,不加锁 读取的数据不一定是最新版本,但是事务内的所有查询读取数据都是同一版本的行数据,不存在脏读、不可重复读及幻读的情况 | | |||
| | |||
2.3 Read Repeatable
所有事务隔离级别设置: set session transaction isolation level repeatable read ;
RR隔离级别中,SELECT操作支持快照读,所有的UPDATE/DELETE/INSERT加锁,锁类型会新增一个GAP LOCK。
表格 SQL | ||||
| 表格的age列无索引,所以update过程中 全表加X锁,期间全表堵塞UPDATE\DELETE\INSERT | |||
你以为结束了!并没有,这里有趣了! 还会添加两个gap lock ((9,2) ,(21,3)),((21,3), (21,25)) 这里我们单独拎出小表格来分析。 | 同时会在索引 age的值上添加 3个 gap lock,分别为 ((4,4),(7,5))、((7,5),(9,2))、((9,2),(21,3)) | |||
以为跟上面的加锁范围一样,no no no 唯一索引列上 每一个age都是唯一的,也就是age=21只有一个,不会再INSERT一个新的 age =21进来,故在这里不需要加gap lock,加锁情况如下: age=21 行添加行锁 X | 但是,范围查询添加到gap lock在其他情况下跟非唯一索引会有一些差别,可以看下表的例子。 |
这里做亮点补充说明:
2.3.1 RR下的非唯一索引加锁情况
update tbname set name=... where age = 21
还记得上篇文章说过 RR隔离级别可以防止 幻读吗?因为在RR隔离级别中,加多了next-key = record lock + gap lock,gap lock是加在索引值之间的锁。也就是 当修改 age=21 的行数据时,除了 在 age=21 这一行添加 X record lock , 还在 ((9,2) ,(21,3)),((21,3), (21,25))这两个age值得范围内添加 gap lock。加锁的情况是:tb_index添加 IX意向锁,age索引上添加age=21的 x record lock,再在主键上的行记录 id=5 添加 X record lock,同时在 age 值上添加两个 gap lock,分别为((9,2) ,(21,3)),((21,3), (21,25))。
注意这里有个误区,很多小伙伴会认为,那么这么加gap锁,则意味着,当update age=21这一列时, 9<age<25 ,这个范围内,是不允许进行 UPATE/DELETE/INSERT的。这种推测实际上是不完整的,因为它没考虑到跟主键!!!
注意,每次写gap lock的时候,都是有加上主键值的。比如这里,当更新 age=21这列时,加了 ((9,2) ,(21,3)),((21,3), (21,25)) 这两个范围的 GAP LOCK,那么在当前update age=21的事务还没有结束的情况下,假设有两条修改SQL的语句:
update tbname set age=9 where id = 1;
update tbname set age=9 where age = 4;
这两条SQL,是能够正常执行,还是堵塞呢?
innodb中,索引按照二叉树排列顺序,而这两条SQL修改后在IX_AGE上的索引值分别为:(9,1)、(9,4),可以发现(9,1)在键值(9,2)的左边,不在GAP LOCK的范围内,所以,可以正常执行;而(9,4)在键值的右边,刚好在GAP LOCK的范围内,会被堵塞!总结:第一条UPDATE SQL,正常秩序;第二条UPDATE SQL会被堵塞。
所以,考虑GAP LOCK的时候,一定要注意结合整个索引键值来分析,而索引键值=索引值+主键。
2.3.2 RR下的唯一索引加锁情况
update tbname set name=... where age between .. and ...
因为唯一索引上面的索引键值都是唯一的,故不会出现重复值的插入的情况,下表罗列了同样的 范围查询修改语句,在唯一索引及非唯一索引上加 GAP_LOCK的情况。
加GAP_LOCK的情况如下(注意注意,方便查看,省略了主键值,实际上是需要添加上主键键值的):
where age between 1 and 7 | where age between 2 and 7 | where age between 5 and 10 | where age between 15 and 50 | |
(-∞,2),(2,4),(4,7),(7,9) | (4,7),(7,9),(9,21) | (9,21),(21,25),(25,+∞) | ||
(-∞,2),(2,4),(4,7) | (2,4),(4,7) |
2.4 Read Serializable
所有事务隔离级别设置: set session transaction isolation level Serializable ;
不支持快照读,所有SELECT都是当前读,所有SELECT操作都需要加S锁,除主键定值查找\唯一索引定值查找外,其他基于索引或者主键的范围查找都会添加 S GAP LOCK。并发度是四个隔离级别中性能最差的。 | ||||
在age索引上 添加两个gap lock ((9,2) ,(21,3)),((21,3), (21,25)) | 同时会在索引 age的值上添加 3个 gap lock,分别为 | |||
这里详细的来分析下 RS 隔离级别下的SELECT操作加的锁:
表格 SQL | where id=5 | where id betwee 5 and 15 | where age=21 | where age betwee 5 and 9 |
主键定值查找 表格tbname 添加 IS 意向锁 id=5 添加 S锁 | 主键范围查找 id=5,id=6 两行数据 添加 S锁 同时添加2个 S GAP LOCK ,分别为 ((5,7),(6,25))跟((6,25),+∞) | 全表查找 表格 tbname 添加 IS 意向锁 由于全表查找,整个表格 再次添加 S 表锁 | 由于全表查找,整个表格 再次添加S 表锁 | |
ix_age索引查找 表格tbname 添加 IS 意向锁 索引上 age = 21 添加 S 行锁 主键上 id=3 添加 S 行锁 同时添加 2个 S GAP LOCK ,分别为 ((9,2) ,(21,3)),((21,3), (21,25)) | age索引上面,age=7,age=9 行添加行锁 S 再在主键上,给id=2,id=5 这一行数据,添加行数 S 同时会在索引 age的值上添加 3个 S gap lock,分别为 | |||
由于age列唯一,故不需要添加GAP LOCK | 同时会在索引 age的值上添加 2 个 S gap lock,分别为 ((4,4),(7,5))、((7,5),(9,2)) |
至此,已说明了四个隔离级别是如何加锁,那么,释放锁呢?
在MySQL INNODB中,遵循的是 strong strict 2-PL,也就是所有的write lock 跟read lock 都是在 事务 commit后才释放。
3 SQL分析
考虑到下文的例子,这里补充两个概念。
ICP:
MRR:
假设表格 tb_lock ( id int primary key not null, age int,score int,name varchar(10), key ix_age_score ( age, score ) ) 数据修改如下 :
假设MySQL当前的隔离级别为 RR,执行 UPDATE tb_index WHERE age between 5 and 22 and score between 1 and 10 and name is not null,其执行计划如下:
那么,是如何加锁的呢?
首先,可以看到是根据索引 ix_age_score 查找,那么分为两种情况来分析,第一种,数据库支持ICP;第二种,数据不支持ICP。
3.1 支持ICP情况
当数据库支持ICP的时候,根据复合索引第一列 age 查找 age between 5 and 22,然后在索引内部过滤 score between 1 and 10后,取得索引值后,如果数据库支持 MRR,则会把取得的索引值放到buffer中,对主键进行排序,然后可以根据顺序的主键值去 主键中查找行数据,如果不支持,则跳过这一步排序步骤,直接根据索引值内部的主键值,查找主键行数,最后过滤 name is not null 。
加锁过程如下,tb_lock添加 IX 意向锁,在索引 ix_age_score 给索引值(7,10,5),(21,4,3)添加上 X record lock,并添加4个 X GAP LOCK,如图片红色素箭头展示,分别为((4,7,4), (7,10,5)),((7,10,5), (9,15,2)),((9,15,2),(21,4,3)),((21,4,3),(25,1,6)),最后在主键上给id=3及id=5 两行数就添加X record lock。
3.2 不支持ICP情况
当数据库不支持ICP的时候,根据复合索引第一列 age 选择 age between 5 and 22,然后根据筛选的索引值 (7,10,5),(9,15,2),(21,4,3)中的主键 5、2、3,找到对应的行数据,再在行数据中 过滤 score between 1 and 10 and name is not null。
加锁过程如下,tb_lock添加 IX 意向锁,在索引 ix_age_score 给索引值(7,10,5),(9,15,2),(21,4,3)添加上 X record lock,并添加4个 X GAP LOCK,如图片红色素箭头展示,分别为((4,7,4), (7,10,5)),((7,10,5), (9,15,2)),((9,15,2),(21,4,3)),((21,4,3),(25,1,6)),最后在主键上给id=2、id=3、id=5 两行数就添加X record lock。
参考文档:http://hedengcheng.com/?p=771#_Toc374698321
如果转载,请注明博文来源: www.cnblogs.com/xinysu/ ,版权归 博客园 苏家小萝卜 所有。望各位支持!