-
MySQL 锁有哪些
从类型上来看,可以分为共享锁、排它锁
从范围来看,可以分为表锁、行锁,间隙锁、页锁等。
其中表锁中又有意向锁。
以上锁根据存储引擎不同,生效的锁也不同。
-
什么是间隙锁
我们首先先定义下关于下面描述的前提场景,首先是基于InnoDB存储引擎,Repeatable Read 隔离级别下的。从名字我们可以猜测出,间隙锁,锁定的是某块间隙,但是锁定的是那块间隙呢?什么场景会触发间隙锁呢?网上有很多关于间隙锁的原理讲解,但有的描述不严谨,下面就带着疑问,跟随一起一步步验证下吧。(PS:下面描述的内容不全是间隙锁,当时验证的时候出了一些自认为很有意思的问题,就记了下来和大家分享,知晓的朋友请自动忽略)
-
间隙锁锁定范围
首先定义如下表
-
CREATE TABLE `gap_lock` (
id bigint(20) NOT NULL,
number int(11) NOT NULL,
PRIMARY KEY (id) USING BTREE,
INDEX idx_number(number) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
插入相应的数据
INSERT INTO `gap_lock`(`id`, `number`) VALUES (1, 1);
INSERT INTO `gap_lock`(`id`, `number`) VALUES (3, 3);
INSERT INTO `gap_lock`(`id`, `number`) VALUES (5, 5);
INSERT INTO `gap_lock`(`id`, `number`) VALUES (8, 8);
锁定某行数据
select * from `gap_lock` where number = 3 for update;
此时,我们开启一个新的会话,验证下面的sql
insert into `gap_lock` values(2,2);#阻塞
insert into `gap_lock` values(4,4);#阻塞
insert into `gap_lock` values(6,5);#非阻塞
insert into `gap_lock` values(0,1);#非阻塞
从上面的执行SQL情况可以看出,间隙锁锁定范围是number 数值(1,3)和 (3,5),开区间。我们再执行下下面的sql
insert into `gap_lock` values(2,1);#阻塞
纳尼,不是开区间么?不包含number等于3和5 么?难道是不单单锁定二级索引number,还锁定了主键索引的范围,整个锁定范围ID是(1,3)和(3,5),number范围是(1,3)和(3,5)?如果是这样的话,那么怎么的一个判断逻辑呢?是如下的判断逻辑么
boolean idLocked = (id > 1 && id < 3) || (id > 3 && id < 5);
boolean numberLocked = (number > 1 && number < 3) || (number > 3 && number < 5);
if(idLocked && numberLocked){
return true;
}
我们继续验证
insert into `gap_lock` values(2,6);#非阻塞
???首先可以确定的是上面的假想有问题,而且此时会有疑问,为什么(2,6)在第四行,不是默认按照id升序排序么,怎么会按照number排序,难道还和排序有关,此时一脸懵逼.jpg。我们继续验证
insert into `gap_lock` values(6,1);#阻塞
(6,1)在第二行,确实出现在我们以前画的锁定的间隙范围内,看来首要问题就是要解决下排序问题了,搞清楚排序问题,应该问题就迎刃而解了。
我们来看看查询的执行计划
explain select * from `gap_lock`;
通过执行计划,发现原来是使用了覆盖索引机制,怪不得排序不是按照主键进行的排序。我们回顾下innodb的索引结构。
树的内节点存储索引的key,叶子节点存储key和具体的数据信息,如果是二级索引,data是记录的主键值,如果是主键索引的话,data的是包含事务ID、回滚ID、版本号以及行数据信息等。
由于IO操作耗时,提高性能的可以通过减少磁盘的IO次数,目前这张表刚好只有两个字段,二级索引number其实可以覆盖所有的行数据信息,读取number索引可以达到查询全部字段的效果,相对来说,二级索引相较于主键索引存储更密集,磁盘IO读取次数更少。
知晓了排序的问题,那么间隙锁锁定范围就突然清晰了
在number这个索引上,排序是如上图,优先按照number进行排序,如果number相同,则按照id排序,大致判断逻辑如下
Node lockNode = new Node(3,3);
Node preNode = lockNode.pre;//(1,1)
Node nextNode = lockNode.next;//(5,5);
/**
* 校验是否被间隙锁锁住,不能执行插入操作
* @param node 待插入的索引节点
* @return true 表示不允许执行插入操作,false表示可以执行插入操作。
**/
public boolean checkGapLock(Node node){
//锁定的行是最后一行
if(nextNode == null){
if(node.key > preNode.key){
return true;
}else if(node.key == preNode.key && node.val > preNode.val){
return true;
}
return false;
}
//锁定的是中间某行(该索引节点不一定存在)
if(node.key > preNode.key && node.key < nextNode.key){
return true;
}else if(node.key == preNode.key && node.val > preNode.val){
return true;
}else if(node.key == nextNode.key && node.val < next.val){
return true;
}else{
return false;
}
}
怎样触发间隙锁的呢?
网上很多描述,例如select * from table where index = xx for update,又或者删除某条不存在的记录等。二级索引&非唯一索引 ,如果某个叶子节点(若不存在则假设它存在)被添加了排它锁,那么就会在该叶子节点周围添加间隙锁,如果刚好其它会话要在间隙锁范围内创建索引,那么则会被挂起。
主键索引& 唯一索引,如果要添加排它锁的节点存在,不会触发间隙锁,如果不存在,则触发间隙锁。
-
为什么主键索引或者唯一索引与普通的索引在间隙锁上的机制有差别呢?我们先来看看什么是幻读和不可重复读。
幻读和不可重复读从结果来看,都是针对一个事务中,多次读取的结果与目标结果不一致。从解决的目的上,是区分不了幻读和不可重复读的。那么从范围上去划分。
从范围上区划分,分为指定行多次读取结果不一致,和范围读取结果不一致。
指定行多次读取,为了解决结果不一致的问题,最简单的办法是加锁(悲观锁)和多版本控制(MVCC)(乐观锁)来实现。我(个人理解,可能不正确)称指定行多次读取结果不一致,叫做不可重复读。
范围多次读取,这样简单的添加行锁是不可能解决多次读取结果不一致的问题,因此需要在范围添加锁。我(个人理解,可能不正确)称范围多次读取结果不一致的问题,叫做幻读。
这也就可以解释,为什么唯一索引、或者主键索引,当要删除、修改一个不存在的行时,触发了间隙锁,锁定了某个范围,而记录存在时,只是触发了行锁(可以明确指定行)。从索引的概念来讲,可以明确锁定某个索引的,且该索引值不会出现重复的,就是不可重复读的问题,不能明确锁定某个索引、或者某个索引值是可重复的,那么就会触发间隙锁,也是幻读的问题。
-
思考
- 假设我们又新增了一个字段val,该字段也添加了索引(非唯一索引),如果执行下面的语句会触发间隙锁么?如果触发,是number索引添加间隙锁,还是val索引添加间隙锁,还是都添加呢?
select * from `gap_lock` where number = 3 and val = 3 for update;