深入理解Mysql(八):锁

105 阅读10分钟

一.概略


通过前面的7篇文章的学习,我们已经学习了msyql的整体架构,读写流程,接下来要进去到msyql中最复杂,最晦涩难懂的知识点--锁.

锁里面的概念太多了,分类也很多,之间的是否兼容也较为复杂,在这块,本人也是不停的挤出来时间来学习这块,感觉自己掌握的也不是非常的清楚,一起加油学习吧

二.mysql的锁

一句话说明锁的本质: mysql的锁是处理写写冲突的

前面学习的MVCC是处理读写冲突的,针对写写冲突还是需要加锁的

2.1 锁的分类

锁根据划分的角度不同,也会有不同的分类:

1.从加锁机制上来分分为乐观锁和悲观锁

乐观锁:从本质上讲,是无锁的,因为他总是认识在修改数据的时候,没有别人修改这个数据,如果修改失败的话,会进行一定的重试,一般是通过版本号和时间戳来判断是否发生更新冲突; 悲观锁:悲观锁是加锁的,因为他总是悲观的认为自己在修改中途中,一定会有人在修改,所以每次都会加锁

2.从锁粒度上,分为表锁和行锁

表锁:表锁是指对一整张表加锁。表锁由MySQL Server层实现

行锁:锁是对表中的某一行,多行,或者行之间的间隙加锁的,是由引擎层实现

3.储存引擎,分为innodb和myisam

这两个是我们常用的引擎,MYISAM没有行锁,只有表锁,INNODB不但有表锁,还有行锁,行锁能增加并发能力

4.按照兼容性分为共享锁和排它锁

共享锁(Share Lock,简称S锁):对锁住的数据,允许其他的事务来读取这个数据,但是不允许修改这个事务;

排它锁(Exclusive Lock,简称X锁):对锁住的数据,不允许其他的事务来读取和修改这个数据,具有排他性


下面我会重点介绍一些常用的锁

2.2 行锁

行锁是INNODB的锁,其他的存储引擎像MYISAM和MEMORY等,是没有行锁的; 行锁根据兼容性分为共享锁和排他锁,根据类型分为RECORD,GAP,next-key锁;

共享锁:像我们平时执行的普通的sql,select * from book; 这种是不带锁的,如果需要加共享锁,需要显示的写sql:select * from book lock in share mode,这样就会给这些数据添加共享锁,共享锁能允许其他的共享锁查询,但是不允许增删改操作,会被阻塞到当前的锁释放.

排他锁:select * from book for update; 就会为该数据添加排他锁,排它锁添加后,是不允许其他增删改和共享,排他锁的

兼容性

是否兼容共享锁排它锁
共享锁兼容不兼容
共享锁不兼容不兼容

记录锁(Record Lock):更新数据时候,通过前面学习,首先会把数据从磁盘加载在内存中,在更新的时候,会在内存中创建一个锁结构,来判断该数据上是否有锁;

如果在update时候,过滤条件不走索引的话,会升级为表锁,最终导致效率低下,在日常开发中,应该注意

间隙锁(Gap Lock):间隙锁存在的目的是为了防止幻读,间隙锁是一个索引值的左开右开的区间'

Next-key Lock: 间隙锁是左开右开的区间,不会包含具体的数据,如果要把具体的数据包含在内,也就是 Next-key Lock = Record Lock +GAP lock;

插入意向锁(Insert Intention Lock):插入意图锁是一种间隙锁,在行执行INSERT之前的插入操作设置,如果多个事务INSERT到同一个索引间隙之间,但没有在同一位置上插入,则不会产生任何的冲突

三者的关系如图所示:

image.png

三者兼容性如下图:

image.png

重点记下面几个就行:

RECORD和RECORD 不兼容,原因:举例,id=1的数据上添加了RECORD数据,另外事务再次修改 id=1的数据,再次添加record锁的时候,肯定是不支持的;

RECORD和 NEXT-KEY不兼容: next-key里面有record锁的,举例 在next-key为(1,10]的区间,修改 id=10的数据, 然后再次修改id=10的数据,这个时候就不允许,所以不兼容

NEXT-KEY NEXT-KEY不兼容,同上,因为NEXT-KEY里面包含record,相当于简化为RECORD和RECORD的不兼容

|| GAP 和GAP/next-key 不兼容:原因, 当GAP或者next-key的时候,说明是对一段间隙进行加锁的,这个时候对这个间隙插入数据,肯定是不允许的,比如 gap 区域是 (1,10),在这个时候查询数据 id=5,肯定是阻塞的


|| GAP 和|| GAP不阻塞的原因:只要插入的位置不是相同的,是不会阻塞的,比如事务1插入数据 id=4,事务2插入数据id=5,这两个数据不在同一位置,肯定是允许添加的;

在之前的理解中,我以为GAP和gap也是不兼容的,通过测试,发现gap和gap是兼容的,如下图所示:

image.png

2.3 表锁

2.3.1 读写意向锁

读写意向锁来检测表锁和行锁的冲突

当数据在行上面添加对应的读锁和写锁的时候,会在表上对应的添加读写意向锁,如果表在执行DDL语句的时候(比如 alter table xxx 或者drop table xxx),会对表添加排它锁,在添加排它锁之前需要判断表中的行是否是锁,如果一行一行遍历所有行来获取是否有锁,效率将会大大降低,这个时候只需要判断表上是否有意向锁,就能快速判断表中的行的锁情况,大大提升效率;

意向锁也是表级锁,分为读意向锁(IS锁)和写意向锁(IX锁)

当事务要在记录上加上行锁时,要首先在表上加上意向锁。这样判断表中是否有记录正在加锁就很简单了,只要看下表上是否有意向锁就行了,从而就能提高效率

意向锁也是表级锁,分为读意向锁(IS锁)和写意向锁(IX锁)

2.3.2 自增锁

AUTOINC 锁又叫自增锁(一般简写成 AI 锁),是一种表锁,当表中有自增列(AUTOINCREMENT)时出现。当插入表中有自增列时,数据库需要自动生成自增值,它会先为该表加 AUTOINC 表锁,阻塞其他事务的插入操作,这样保证生成的自增值肯定是唯一的

image.png

三.案例说明锁的作用

建表语句:

CREATE TABLE book (

id int(11) NOT NULL,

isbn varchar(255) DEFAULT NULL,

author varchar(255) DEFAULT NULL,

score double(2,1) DEFAULT NULL,

PRIMARY KEY (id),

UNIQUE KEY isbn_unique_index (isbn) USING BTREE,

KEY author_index (author) USING BTREE

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO book(id, isbn, author, score) VALUES (10, 'N0001', 'Bob', 3.4); INSERT INTO book(id, isbn, author, score) VALUES (18, 'N0002', 'Alice', 7.7); INSERT INTO book(id, isbn, author, score) VALUES (25, 'N0003', 'Jim', 5.0); INSERT INTO book(id, isbn, author, score) VALUES (30, 'N0004', 'Eric', 9.1); INSERT INTO book(id, isbn, author, score) VALUES (41, 'N0005', 'Tom', 2.2); INSERT INTO book(id, isbn, author, score) VALUES (49, 'N0006', 'Tom', 8.3); INSERT INTO book(id, isbn, author, score) VALUES (60, 'N0007', 'Rose', 8.1);

id为主键,isbn是唯一二级索引,author是为二级非唯一索引,score(评分)无索引,数据如下:

image.png

3.1 聚簇索引命中情况

UPDATE book SET score = 9.2 WHERE ID = 10;

这个时候会对id=10的这条数据添加RECORD锁,如图:

image.png 这个是最简单的,测试

image.png

3.2 聚簇索引,未命中情况

事务1: UPDATE book SET score = 9.2 WHERE ID = 16;

事务2: INSERT INTO book(id, isbn, author, score) VALUES (17, 'N00017', 'Alice', 7.7);

结果:事务2会阻塞掉,直到事务1提交了事务

如图所示: image.png

原因如下: 在 RC 隔离等级下,不需要加锁;而在 RR 隔离级别会在 ID = 16 前后两个索引之间加上间隙锁,而事务2要添加id=17的数据,根据前面的锁的兼容关系,我们得知GAP锁和RECORD是不兼容的,所以事务2会阻塞直到事务1提交事务;

image.png

image.png 从上图的测试也能得知GAP锁和GAP锁是兼容的,和我们上面的总结是一样的;

3.3 二级唯一索引,查询命中

UPDATE book SET score = 9.2 WHERE ISBN = 'N0003'

因为INNODB的二级索引,叶子节点保存的是主键的值,所以会有一个二次查找的过程,这个时候,对二级索引和主键索引都会添加RECOED锁 image.png

3.4 二级唯一索引,查询未命中

UPDATE book SET score = 9.2 WHERE ISBN = 'N0008'

RR 隔离等级下未命中时的加锁情况,RC 隔离等级下该语句未命中不会加锁。

执行结果如下:

image.png

原因分析: 事务1执行UPDATE book SET score = 9.2 WHERE ISBN = 'N0008';由于没有命中,并且N008在N007后面,所以会添加一个间隙锁 (N007,正无穷),事务2在执行数据INSERT INTO book(id, isbn, author, score) VALUES (61, 'N0009', 'Rose', 8.1);时候,由于N009也在这个间隙锁内,所以无法插入成功,需要事务1提交后才能执行;

image.png

3.5 二级非唯一索引,查询命中

执行语句:UPDATE book SET score = 9.2 WHERE Author = 'Tom'

在 RC 等级下,二级唯一索引和二级非唯一索引的加锁情况是一致的,都是在涉及的二级索引和对应的主键索引上加上排他记录 image.png

但是在 RR 隔离等级下,加锁的情况产生了变化,它不仅对涉及的二级索引和主键索引加了排他记录锁,还在非唯一二级索引上加了三个间隙锁,锁住了两个 Tom 索引值相关的三个范围;

那为什么唯一索引不需要加间隙锁呢?间隙锁的作用是为了解决幻读,防止其他事务插入相同索引值的记录,而唯一索引和主键约束都已经保证了该索引值肯定只有一条记录,所以无需加间隙锁。

测试结果如下:

image.png

3.6 二级非唯一索引,查询未命中

UPDATE book SET score = 9.2 WHERE Author = 'Sarah' 在 RR 隔离等级下未命中的加锁情况,它会在二级索引 Rose 和 Tom 之间加间隙锁。而 RC 隔离等级下不需要加锁。

image.png

3.7 无索引

UPDATE book SET score = 9.2 WHERE score = 22

当 Where 从句的条件并不使用索引时,则会对全表进行扫描,在 RC 隔离等级下对所有的数据加排他记录锁。在RR 隔离等级下,除了给记录加锁,还会对记录和记录之间加间隙锁。和上边一样,间隙锁会和左侧的记录锁合并成 Next-Key 锁

image.png

3.8 聚簇索引,范围查询

UPDATE book SET score = 9.2 WHERE ID <= 25

RC 场景下与等值查询类似,只会在涉及的 ID = 10,ID = 18 和 ID = 25 索引上加排他记录锁。

image.png 而在 RR 隔离等级下则有所不同,它会加上间隙锁,和对应的记录锁合并称为 Next-Key 锁。除此之外,它还会在(25, 30] 上分别加 Next-Key 锁

3.9二级索引,范围查询

UPDATE book SET ISBN = N0001 WHERE score <= 7.9

image.png

四.总结

msyql锁在索引的加锁情况,可以整理如下:

image.png mysql锁的分类很多,细节很多,各种锁之间兼容性也较为复杂,还需要进一步的学习和领悟 太晚了,都23点多了,要睡啦