这是学习笔记的第 2015 篇文章
今天引用一下我书稿中的一部分内容,关于锁的东东。
InnoDB的锁,实现了两种类型的行锁。
l 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同的数据集的排他锁。
select * from table_name where .....lock in share mode
l 排他锁(X):允许获得排他锁的事务更新数据,但是组织其他事务获得相同数据集的共享锁和排他锁。
select * from table_name where .....for update
在此我们可以设想一个场景,有两个事务A和B,事务A锁住了表中的一行,加了行锁S,即这一行只能读不能写。
之后事务B申请整个表的写锁(MySQL Server层可以使用lock table xxxx write的方式加写锁锁表),那么理论上它就能修改表中的任意一行,包括共享锁S锁定的那一行,这种情况下和事务A持有的行锁是冲突的,这种情况下,就需要有一种机制来判断,避免这个冲突,比如我们需要先判断表是否被其他事务用表锁锁定,然后判断表中的每一行是否被行锁锁住,显然这种情况下是不可接受的,问题的瓶颈就在于需要遍历整个表,随着数据量的增加,这个代价就会无限放大,在这种情况下,意向锁就是来做这个冲突协调者的。
所以一个正常的流程就会变为:
l 事务A必须先申请表的意向共享锁,成功后申请一行的行锁
l 事务B申请排它锁,但是发现表上已经有意向共享锁,说明表中的某些行已经被共享锁锁定了,事务B申请写锁的操作会被阻塞。
而这也是为什么需要表级意向锁的主要原因,InnoDB有两个表级意向锁:
l 意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁
l 意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
整个表级意向锁的加锁过程是自动完成的,我们可以举个例子来说明下,比如我们生活中的红绿灯,一般路灯红绿灯切换是不会马上切换的,而是会转为黄色,转为黄色后,会有几秒钟的缓冲时间,而这些就是留给了行人和司机的准备时间,表级意向锁的角色和这个是类似的。
说完表级意向锁,我们继续来说行锁。
InnoDB行锁是通过给索引项加锁实现的,如果没有索引,InnoDB会通过隐藏的聚簇索引来对记录加锁。
如果不通过索引条件检索数据,那么InnoDB将对表中所有数据加锁,实际效果跟表锁一样。
InnoDB支持如下的三种行锁定方式:
l Record lock:对索引项加锁,即锁定一条记录;
l Gap lock:对索引项之间的‘间隙’、对第一条记录前的间隙或最后一条记录后的间隙加锁,即锁定一个范围的记录,不包含记录本身;
l Next-key Lock:锁定一个范围的记录并包含记录本身。
Next-Key Lock是行锁与间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。如果一个间隙被事务加了锁,其它事务是不能在这个间隙插入记录的。
到目前为止,我们也说了几种锁了,这些锁之间是什么样的兼容关系,可能有的同学会有些迷糊,MySQL里的锁兼容列表大体是这样的关系,我们需要明确:意向锁之间是互相兼容的,这句话很重要。按照这个思路里面一半的内容就明确了。而另外一部分则是S和X的兼容性。带着S锁和X锁的组合都是互相排斥,而S锁之间是互相兼容的。所以下图7-10按照这个思路几乎不用记就能基本理解了。
此外就是死锁,如果锁不兼容的情况下,通常会产生阻塞,而如果产生互相阻塞的场景,那就是死锁了。
如图所示,这是一种经典的死锁检测机制:wait-for graph算法
我们来看一个死锁的小例子,在两个会话并发的场景下,死锁的步骤如下:
首先创建一张表dt1,语句如下:
create table dt1 (id int unique);
然后按照下表的方式来操作。
时间 | session1 | session2 |
---|---|---|
T1 | begin;select *from dt1 lock in share mode; | |
T2 | begin;select *from dt1 lock in share mode; | |
T3 | insert into dt1 values(1); --阻塞 | |
T4 | insert into dt1 values(2); 产生死锁 |
所以上面的语句特点很明显,插入的数据分别是1和2产生了死锁,我们可能很少看到直接声明share mode的方式,但是有很多时候由其他的场景会触发,比如对于duplicate数据的检查会开启S锁。这是比较特别的一点,需要注意。