S锁和X锁都有表级和行级的区别,行级的不是加载记录上的,而是加在该索引B+树的叶子节点上的。
加读/写锁之前,必须先加意向读/写锁,就像是你做事情,是先有想法,再付出行动。锁也是一样,你先表明你想要加锁,这样别的事务会先看到你的意向锁,就不会去想着插入或者修改了,没有意向锁,其它事务会遍历查看是否有被锁的记录,这样很消耗性能。
锁一般怎么表示:
加锁的流程(MDL 元数据锁,图上写错了)
主键加锁的情况:
聚簇索引的B+树,加锁流程是先给表加IX锁,然后找记录,最后再页55上的id=6的记录上加X锁(这一步是加在B+树叶子节点上的)
非主键加锁的情况:
这种情况下会加两次锁,第一次是在二级索引树上加表级锁 意向写锁IX
,然后正常的通过二叉搜索找到叶子节点对应的记录,加独占写锁X
,接着拿到主键索引进行回表,给主键索引的B+树加意向写锁IX
,然后根据主键索引值找到记录行,加独占写锁X
,写完释放锁,结束。
1.行锁
行锁,所有作用在行上的锁都叫行锁,锁基本都作用在索引上,所以这里的行锁都是作用在索引上的,不管是锁二级索引的B+树还是锁聚簇索引的B+树。
1.1.共享读锁(S锁
):
共享读锁,存在于可重复读事务隔离级别中,顾名思义就是允许多个事务同时读,但读的时候不允许其它事务来写。很简单嘛,如果我读的时候,别人写了那隔离级别不久变了嘛,允许的话就成了读已提交的事务隔离级别了。
很简单的一个概念,但又很重要。
1.2.排他锁(X锁
):
排他锁又叫行锁,行锁(如果要加锁)永远都是加在索引的叶子节点上,不会有其它情况,如果一张表你没有设置主键,那就会加在隐藏列上(MySQL自行创建的一个主键列,用户不可见)。
行锁共有四种(这里说行锁,就不管表级的意向写锁IX
了,IX
会在所有的X
之前加)
第一种-where后条件为主键(where id = xxx
):最基础的行锁,此时行锁会加在主键列上,就是主键索引的B+树的叶子节点上。
第二种-where后面条件是二级索引(where username = xxx
):是二级索引的情况下,会加两个锁,第一把锁会加在二级索引对应B+树的叶子节点上。然后,拿主键索引回表,第二把锁会加在主键索引B+树的叶子节点上。
第三种-where后面没有条件(update set xxx;
):这种情况下其实是在做全表更新,所以会给这张表的所有记录都加锁,加锁的位置是主键B+树的对应叶子节点(其实就是所有的叶子节点都加锁)。
第四种-where后条件为非索引列(where gender = 1;
):这种情况下,会先给这张表种所有记录的主键B+树都都加锁,然后判断当前行是否满足条件,不满足就立即释放锁,满足就下一条(就是全表扫描,满足的留下,不满足的释放)。
1.3.间隙锁(Gap Lock
):
间隙锁又叫范围锁,顾名思义就是加在缝隙里的锁(或者锁定一个范围),它可以加在两条记录之间,或者某条记录之前(或之后),目的是为了避免幻读问题。幻读大概意思就是一个事务要访问多行的数据,有事务A需要做年龄统计,有一个SQLselect count(1) where age > 18;
,很明显统计了表中所有年龄大于18岁的用户记录条数,这时候已经查了一次了但一会儿还得查一次。这时候有个新用户注册了,产生了事务B,新用户年龄是20岁,事务B正常插入记录然后commit走了,此时事务A还没结束,又来查发现count(1)的条数多了一条。这就是幻读,一个事务中两次相同查询操作的结果不一致。
这里也分为两种情况讨论:
第一种-where条件后是主键:这种情况下,MySQL知道你要更新的只有一条记录,主键不能重复、不能为空嘛,可以确定只会影响到一条记录,此时哪怕是在RR(可重复读)的事务隔离级别下也只会用排他锁,不会用间隙锁(其实RR下用的是临键锁)。
第二种-where条件后面是非唯一索引(二级索引、联合索引、空间索引这些都算):这些情况下,RC隔离级别时会加间隙锁,RR隔离级别下会加临键锁,而且要注意,哪怕你的二级索引精准命中了某一条记录,也不会提升成排他锁,比如update ... where username= "韩大狗";
此时是精准命中了,但二级索引本身是允许重复的,你能保证天下就一个叫韩大狗的么?那肯定不能,MySQL也觉得不能,那可能会命中多条记录,又得防止幻读,可不就只能范围性的加锁了么(RC不用管幻读,所以只加排他锁,但RR得防止幻读,就得用间隙锁+排他锁组成的临键锁)。
这里还得补充一下,二级索引命中零条、一条、两条及以上情况下的锁定范围,一条时会锁定上一条记录到当前记录和当前记录到下一条记录之间的范围:
当一条都不命中时:
age=10(id=1)
age=30(id=3)
执行查询WHERE age=20
时(无命中记录),此时的间隙锁范围是(10, 30)
,原因是InnoDB怕你在事务进行中突然插入age=20
的记录,导致幻读。
当只命中一条记录时:
age=10(主键id=1)
age=20(主键id=2) <-- 唯一命中
age=30(主键id=3)
那么间隙锁就会锁定(10, 20)
和(20, 30)
的范围。
当命中两条及以上记录时:
age=10(id=1)
age=20(id=2) <-- 第一条命中
age=20(id=3) <-- 第二条命中
...
age=30(id=4)
这种情况下间隙锁就会锁定(10, 20)
、(20, 20)
、(20, 30)
的范围,你可能会觉得奇怪,(20, 20)
咋锁啊?看上面的例子,有没有注意到它们的主键值不一样?之前因为图方便,省略了age=
的字样,别忘了age列索引的B+树叶子节点记录的是什么,是age列的值+主键列的值,所以实际锁定的是(age=10&id=1, age=20&id=2)
、(age=20&id=2, age=20&id=3)
、(age=20&id=3, age=30&id=4)
的范围。此时再看是不是就不重复了。
注意,间隙锁仅作用于当前索引列,不会像排他锁那样既锁当前列的B+树又锁主键列的B+树;而主键列因为索引的唯一性,所以没有间隙锁的用法,都是直接用排他锁去锁定某一行的。比如我有SQL:update... id = 1 or id = 2;
虽然命中了两条但不会产生间隙锁的使用,而是用两次排他锁去锁定id=1
和id = 2
的两条记录,因为主键的唯一性,所以插入范围是已知的,id= 1 or id = 2
不可能会命中三条记录自然也就不用锁范围了。
1.4.临键锁(Next-Key-Locks
):
临键锁也叫后码锁,就是排他锁X
和间隙锁组合使用的一种情况。它是为了在可重复读RR
事务隔离级别下,彻底终结幻读问题MySQL所想出来的骚操作。
那为什么单用间隙锁或者单用排他锁解决不了幻读问题呢?先说只用排他锁的情况,前面提到排他锁只能锁定某行嘛,如果中途有事务要来修改或者删除,那其实没问题,你用排他锁依次锁定范围内的所有记录就好,不会有任何问题;但如果其它事务是新增操作呢?你怎么提前锁定一行未知的数据?锁不了,没这个实力你知道吧。所以要用间隙锁来锁定一个范围,这个范围内一条记录别的事务都动不了,你可能说这不挺好么,只用间隙锁就好了呀。但是间隙锁有个问题,就是不能锁定当前行,万一别的事务不动周围的,就要动你这条记录呢?那还是会产生幻读,所以MySQL官方干脆把这俩结合起来用,锁定范围是左开右闭的一个区间,这样一来当前行和临近行都无法操作了,自然就不会有幻读问题了。
既然临键锁是复合型的,那它会不会具有两种锁的特性呢?排他锁锁定当前列和主键列的B+树叶子节点,间隙锁锁定当前列B+树叶子节点的一个范围?欸,还真是,临键锁还真就能跨索引工作,但是哈,这一特点是从排他锁这边继承过来的,目的也是为了让锁的效率更加高效。
关于临键锁的工作原理,就拿之前间隙锁举的例子来说,
当一条都不命中时:
age=10(id=1)
age=30(id=3)
执行更新语句条件是WHERE age=20
时)因为无命中记录,所以排他锁自然锁不上,此时就只会用间隙锁,范围是(10, 30)
。
当只命中一条记录时:
age=10(主键id=1)
age=20(主键id=2) <-- 唯一命中
age=30(主键id=3)
那么间隙锁就会锁定(10, 20)
和(20, 30)
的范围,然后排他锁会锁定age=20
这条记录,所以实际会锁定(10, 20]
和(20, 30)
的范围(这里不对哈,正确的应该是(10, 20]
和(20, 30]
的范围,至于原因是为了防御全局幻读,听不懂看后面)。
当命中两条及以上记录时:
age=10(id=1)
age=20(id=2) <-- 第一条命中
age=20(id=3) <-- 第二条命中
...
age=30(id=4)
这种情况下间隙锁就会锁定(age=10&id=1, age=20&id=2)
、(age=20&id=2, age=20&id=3)
、(age=20&id=3, age=30&id=4)
的范围,然后排他锁会锁定age=20&id=2
和age=20&id=3
的记录,所以实际的锁定范围就是(age=10&id=1, age=20&id=2]
、(age=20&id=2, age=20&id=3]
、(age=20&id=3, age=30&id=4)
。怎么样,是不是一下就通畅了?
并非通畅,这里的值也不对,实际应该是...前面不变(age=20&id=3, age=30&id=4]
的范围。啊嘞?最后这个吊车尾为啥总要带上?明明它都不满足where条件>_<!哎~这里其实还要从临键锁的原理讲起,临键锁是基于索引的物理存储顺序,而不是WHERE条件的逻辑范围。临键锁的范围由当前记录和下一个记录的键值决定,所以最后一个命中记录的下一个键值是age=30,id=4
,所以锁定到那里。是不是还有点懵?那换种说法:假设不锁最后这条age=30,id=4
的记录,会发生什么?我尝试插入一条age=20(id=3.5)
的记录会发生什么?别问id为啥是小数,谁规定float类型的列就不能做主键ID了?结论就是能插入又满足条件,寄,幻读了。那为了避免你往末尾那个位置追加满足条件的记录,所以干脆把最后一个节点也锁上,你就做不了妖了。官话就是:全局防御幻读。
注意哦,这都是可重复读RR
隔离级别下的,读已提交RC
隔离级别下只加排他锁,不加间隙锁。开玩笑,你RR下要解决幻读,关我RC甚么事?我根本就没打算管幻读,我只追求高性能,有什么错?
2.表锁
锁定整张表的锁,这种锁开销小,加锁快,但由于锁定的是整张表,所以并发贼低,官方都说非必要别用。
按照粒度分其实就这两种,无非就是作用在表上或者作用在行上的,所以严格意义上它们不算是一种锁,它们指的是一类锁。
意向锁(Intention-Locks):
2.1.读意向锁(IS)
在加共享读锁(S锁)之前,会先在表上加一把表级的意向读锁(IS),表明当前有事务对该表中的某些记录加了S锁。
2.2.写意向锁(IX)
在加独占写锁(X锁)之前,会先在表上加一把表级的意向写锁(IX),表明当前有事务对该表中的某些记录加了X锁。
2.3插入意向锁(Insert-Intention-Locks)
这又是个啥东西,那么长一坨。插入意向锁,简称II Gap
,这个锁表示一种插入的意向,只有在Insert的时候才会使用。插入意向锁之间不冲突,但是间隙锁会阻塞插入意向锁,插入意向锁不能阻塞间隙锁。
这个锁在InnoDB中是看不到的。
3.元数据锁(MDL)
锁定的是server层的资源,防止重复的DML或者DDL操作。
不想写了,累了,明天再说>_<