解决并发事务带来问题的两种基本方式

并发事务访问相同记录的情况可以划分为3种。

  • 读 - 读情况:并发事务相继续读取相同的记录。读取操作本身不会对记录有任何影响,不会引起什么问题,所以允许这种情况的发生。
  • 写 - 写情况:并发事务相继对相同的记录进行改动。
  • 读 - 写或写 - 读情况:也就是一个事务进行读取操作,另一个事务进行改动操作。

写 - 写情况

在写 - 写情况下会发生脏写的现象,任何一种隔离级别都不允许这种现象的发生。所以在多个未提交事务相继对一条记录进行改动时,需要让他们排队执行。这个排队执行的过程其实是通过为该记录加锁来实现的。这个 “锁” 本质上是一个内存中的结构,在事务执行之前本来是没有锁的,也就是说一开始是没有锁结构与记录进行关联的,如下图所示。
image.png

当一个事务想对这条记录进行改动时,首先会看看内存中有没有这条记录关联的锁结构;如果没有,就会在内存中生成一个锁结构与之关联。比如,事务T1要对这条记录进行改动,就需要生成一个锁结构与之关联。比如,事务T1要对这条记录进行改动,就需要生成一个锁结构与之关联,如图下所示。

image.png

其实锁中有很多信息,不过为了方便理解,我们现在只把两个比较重要的属性拿了出来。

  • trx信息:表示这个锁结构是与哪个事务关联的。
  • is_waiting:表示当前事务是否在等待。

如图上所示,在事务T1改动这条记录前,就生成了一个锁结构与该记录关联。因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false。我们把这个场景称为获取锁成功,或者加锁成功,然后就可以继续执行操作了。

在事务T1提交之前,另一个事务T2也想对该记录进行改动,那么T2先去看看有没有锁结构与这条记录关联。在发现有一个锁结构与之关联后,T2也生成了一个锁结构与这条记录关联,不过锁结构的is_waiting属性值为true,表示需要等待。我们把这个场景称为获取锁失败,或者加锁失败,或者没有成功地获取到锁,如下所示。

image.png

事务T1提交之后,就会把它生成的锁结构释放掉,然后检测一下还有没有与该记录关联的锁结构。如果发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让T2继续执行。此时事务T2就算获取到锁了。

image.png

读 - 写或写 - 读情况

在读 - 写或写 - 读情况下会出现脏读、不可重复度、幻读的现象,怎么避免脏读、不可重复度、幻读这些现象呢?其实有两种可选的解决方案。

  • 方案1: 读操作使用多版本并发控制(MVCC),写操作进行加锁。
    MVCC就是通过生成一个ReadView,然后通过ReadView找到符合条件的记录版本(历史版本由undo日志构建的)。其实就像是在生成ReadView的那个时刻,时间静止了(就像用相机拍了一个快照),查询语句只能读到在生成ReadView之前已经提交事务所做的更改,在生成ReadView之前为提交的事务或者之后才开启的事务所做的更改是看不到的。写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本这两者并不冲突,也就是采用MVCC时,读 - 写操作并不冲突
  • 方案2: 读、写操作都采用加锁的方式。
    如果我们的一些业务中不允许读取记录的就版本,而是每次都必须去读取记录的最新版本。比如在银行存款的事务中,我们需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。再将账户余额读取出来后,就不想让别的事务 再访问到余额,直到本次存款事务执行完成后,其他事务才可以访问账户的余额。这样在读取记录的时候也就需要对其进行加锁操作,这也就意味着读操作和写操作也得想写 - 写操作那样排队执行。

很明显,如果采用MVCC方式,读 - 写操作彼此并不冲突,性能更高;如果采用加锁的方式,读 - 写操作彼此需要排队执行,从而影响性能。但是在某些特殊的业务场景中,要求必须采用加锁的方式执行,那也是没有办法的事。

一致性读

事务利用MVCC进行读取操作称为一致性读(Consistent Read),或者一致性无锁读(有的资料也称之为快照读)。所有普通的SELECT语句(plain SELECT)在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读,比如:

select * from t;
select * from t1 inner join t2 on t1.col1 = t2.col2;

一致性读并不会对表中的任何记录进行加锁操作,其他事务可以自由地对表中的记录进行改动。

锁定读

1.共享锁和独占锁

并发事务的读 - 读情况并不会引起什么问题,不过对于写 - 写、读 - 写或写 - 读这些情况,可能会引起一些问题,需要使用MVCC或者加锁的方式来解决它们。在使用加锁的方式来解决问题时,由于既要允许读 - 读情况不受影戏那个,又要使写 - 写、读 - 写或写 - 读情况中的操作相互阻塞,所以设计MySQL的大叔给锁分了个类。

  • 共享锁(Shared Lock):简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁。
  • 独占锁(Exclusive Lock):也常称为排他锁,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁。

假如事务T1首先获取了一条记录的S锁,之后事务T2接着也要访问这条记录:

  • 如果事务T2想要再获取一个记录的S锁,那么事务T2也会获得该锁,这也意味着事务T1和T2在该记录上同事持有S锁;
  • 如果事务T2想要再获取一个记录的X锁,那么此操作会被阻塞,直到事务T1提交之后将S锁释放掉为止。

如果事务T1首先获取了一条记录的X锁,那么之后无论事务T2是想获取该记录的S锁还是X锁,都会被阻塞,直到事务T1提交之后将X锁释放掉为止。
所以S锁和S锁是兼容的,S锁和X锁是不兼容的,X锁和X锁也是不兼容的。我们通过下表来表示一下。

兼容性X锁S锁
X锁不兼容不兼容
S锁不兼容兼容

2.锁定读的语句

前面说到,为了采用加锁的方式避免脏读、不可重复读和幻读这些现象,在读取一条记录时需要获取该记录的S锁。这其实是不严谨的,有时候我们想在读取记录时就获取记录的X锁,从而禁止别的事务读写该记录。我们把这种在读取记录时就为该记录加锁的读取方式称为锁定读(Locking Read)。设计MySQL的大叔提供了下面两种特殊的SELECT语句格式来支持锁定读。

  • 对读取的记录加S锁:

    select ... lock in share mode;

    也就是在普通的SELECT语句后面加上LOCK IN SHARE MODE。如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样可以允许别的事务继续获取这些记录的S锁(比如,别的事务也适用SELECT ... LOCK IN SHARE MODE语句来读取这些记录时),但是不能获取这些记录的X锁(比如使用SELECT ...FOR UPDATE语句来读取这些记录,或者直接改动这些记录时)。如果别的事务想要获取这些记录的X锁,那么它们会被阻塞,直到当前事务提交之后将这些记录上的S锁释放掉为止。

  • 对读取的记录加X锁:

    select ... for update;

    也就是在普通的SELECT语句后面加上FOR UPDATE。如果当前事务执行了该语句,那么它会为读取到的记录加X锁,这样既不允许别的事务获取这些记录的S锁(比如别的事务使用SELECT ...LOCK IN SHARE MODE语句来读取这些记录时),也不允许获取这些记录的X锁(比如说使用SELECT ...FOR UPDATE语句来读取这些记录,或者直接改动这些记录时)。如果别的事务想要获取这些记录的S锁或者X锁,那么它们会被阻塞,直到当前事务提交之后将这些记录上的X锁释放掉为止。

写操作

平时用到的写操作无非是DELETE、UPDATE、INSERT这三种。

  • DELETE:对一条记录执行DELETE操作的过程其实是在B+树中定位到这条记录的位置,然后获取这条记录的X锁,最后再执行delete mark操作。我们也可以把这个 "先定位待删除记录在B+树中的位置,再获取这条记录的X锁的过程" 看成是一个获取X锁的锁定读。
  • UPDATE:在对一条记录进行UPDATE操作时分为下面3种情况。

    • 如果未修改改记录的键并且更新的列所占用的存储空间在修改前后未发生变化,则先在B+树中定位到这条记录,然后再获取记录的X锁,最后在原记录的位置进行修改操作。其实也可以把这个 "先定位待修改记录在B+树种的位置,然后再获取记录的X锁的过程" 看成是一个获取X锁的锁定读。
    • 如果未修改改记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+树中定位到这条记录的位置,然后获取记录的X锁,之后将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。可以把这个 “先定位待修改记录在B+树中的位置,然后再获取记录的X锁的过程” 看成是一个获取X锁的锁定读,与被彻底删除的记录关联的锁也会被转移到这条新插入的记录上来。
    • 如果修改了改记录的键值,则相当于在原记录上执行DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETE和INSERT的规则进行了。
  • INSERT:一般情况下,信插入的一条记录受隐式锁保护,不需要在内存中为其生成对应的锁结构。

多粒度锁

上文提到的锁都是针对记录的,可以将其称为行级锁或者行锁。一条记录加行锁,影响的也只是这条记录而已,我们就说这个行锁的粒度比较细。其实一个事务也可以在表级别进行加锁,自然就将其称为表级锁或者表锁。对一个表加锁,会影响表中所有的记录,我们就说这个锁的粒度比较粗。给表加的锁也可以分为共享锁(S锁)和独占锁(X锁)。

  • 给表加S锁
    如果一个事务给表加了S锁,那么:

    • 别的事务可以继续获得该表的S锁;
    • 别的事务可以继续获得该表中某些记录的S锁;
    • 别的事务不可以继续获得该表的X锁;
    • 别的事务不可以继续获得该表中某些记录的X锁。
  • 给表加X锁
    如果一个事务给表加了X锁(意味着该事务要独占这个表),那么:

    • 别的事务部不可以继续获得该表的S锁;
    • 别的事务部不可以继续获得该表中某些记录的S锁;
    • 别的事务不可以继续获得该表的X锁;
    • 别的事务部不可以继续获得该表中某些记录的X锁。

上面的文字看着有点啰嗦。为了更好的理解这个表级别的S锁和X锁,我们以大学教学楼中的教室为例来分析加锁的情况。

  • 教室一般都是公用的,我们可以随便选一间教室进去上自习。当然,教室不是自家的,一间教室可以容纳很多同学上自习。每当一个同学进去上自习,就相当于在教室门口挂了一把S锁,如果很多同学都进去上自习,就相当于教室门口挂了很多把S锁(类似行级别的S锁)。
  • 有时教室会进行检修,比如换地板、换天花板、换灯管啥的,这些维修项目并不能同时开展。如果教室针对某个项目进行检修,就不允许同学来上自习,也不允许其他维修项目进行,此时相当于教室门口挂了一把X锁(类似行级别的X锁)。

上面提到的这两种锁都是针对教室而言,不过我们有事会有一些特殊的需求。

  • 有上级领导要来参观教学楼的环境。校领导不想影响同学们上自习,但是此时不能有教室处于维修状态,于是可以在教学楼门口放一把S锁(类似于表级别的S锁)。此时:

    • 来上自习的学生看到教学楼门口有S锁,可以继续进入教学楼上自习;
    • 修理工看到教学楼门口有S锁,则先在教学楼门口等着,等啥时候上级领导走了,把教学楼的S锁撤掉后,再进入教学楼维修。
  • 学校要占用教学楼进行考试。此时不允许教学楼中有正在上自习的教室,也不允许对教室进行维修,于是可以在教学楼门口放置一把X锁(类似表级别的X锁)。此时:

    • 来上自习的学生看到教学楼门口有X锁,则需要在教学楼门口等着,啥时候考试结束,把教学楼的X锁撤掉后,再进入教学楼上自习。
    • 修理工看到教学楼门口有X锁,则先在教学楼门口等着,等啥时候考试结束,把教学楼的X锁车掉后,再进入教学楼维修。

但是这里存在下面两个问题:

  • 如果想对教学楼整体上S锁,首先需要确保教学楼中没有正在维修的教室,如果有正在维修的教室,则需要等到维修结束才可以对教学楼整体上S锁;
  • 如果想对教学楼整体上X锁,首先需要确保教学楼中没有上自习的教室以及正在维修的教室,如果有上自习的教室或者正在维修的教室,则需要等到上自习的所有同学都上完自习离开,以及维修工维修完教室离开后才可以对教学楼整体上X锁。

我们在对教学楼整体上锁(表锁)时,怎么知道教学楼中有没有教室已经被上锁(行锁)了呢?一次检查每一间教室门口有没有上锁?这效率也太慢了吧!遍历是不可能遍历的,这辈子都不可能遍历的。于是设计InnoDB的大叔提出了一种称为意向锁(Intention Lock)的东西。

  • 意向共享锁(Intention Shared Lock):简称IS锁,当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
  • 意向独占锁(Intention Exclusive Lock):简称IX锁,当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。

视角回到教学楼和教室上来:

  • 如果有学生到教室中上自习,那么他现在整栋教学楼门口放一把IS锁(表级锁),然后再到教室门口放一把S锁(行锁);
  • 如果有维修工到教室进行维修,那么他现在整栋教学楼门口放一把IX锁(表级锁),然后再到教室门口放一把X锁(行锁)。

之后:

  • 如果有上级领导要参观教学楼,也就是想在教学楼门口前放S锁(表锁)时,首先要看一下教学楼门口有没有IX锁;如果有,则意味着有教室在维修,需要等到维修结束把IX锁车掉后,才可以在整栋教学楼上加S锁;
  • 如果有考试占用教学楼,也就是想在教学楼门口前放X锁(表锁)时,首先要看一下教学楼门口有没有IS锁或IX锁;如果有,则意味着有教室正在上自习或者在维修,需要等到学生们上完自习或者维修结束把IS锁和IX锁车掉后,才可以在整栋教学楼上加X锁。
贴士:学生在教学楼门口加IS锁时,是不关心教学楼门口是否有IX锁的;维修工在教学楼加IX锁时,是不关心教学楼门口是否有IS锁或者其他IX锁的。IS锁和IX锁只是用来判断当前教学楼里有没有被占用的教室,也就是只有在对教学楼加S锁或者X锁后才会用到。

总结一下:IS锁、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录;也就是说其实IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。
下面画个表来看一下表级别的各种锁的兼容性。

兼容性X锁IX锁S锁IS锁
X锁不兼容不兼容不兼容不兼容
IX锁不兼容兼容不兼容兼容
S锁不兼容不兼容兼容兼容
IS锁不兼容兼容兼容兼容

MySQL中的行锁和表锁

其他存储引擎中的锁

对于MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁,而且这些存储引擎并不支持事务,所以当我们为使用这些存储引擎的表加锁时,一般都是针对当前会话来说的。

比如在Session 1中对一个表执行SELECT操作,就相当于为这个表加了一个表级别的S锁。如果在SELECT操作未完成时,在Session 2中对这个表执行UPDATE操作,相当于要获取表的X锁,此操作将会被阻塞。直到Session 1中的SELECT操作完成,释放掉表级别的S锁后,在Session 2中对这个表执行UPDATE操作才能继续获取X锁,然后再执行具体的更新语句。

贴士:因为使用MyISAM、MEMORY、MERGE这些存储引擎的表在同一时刻只允许一个会话对表进行写操作,所以这些存储引擎实际上最好用在只读场景下,或者用在大部分都是读操作或者单用户的场景下。

InnoDB存储引擎中的锁

InnoDB存储引擎支持表级锁,也支持行级锁。表级锁粒度粗,占用资源较少。不过有时我们仅仅需要锁住几条记录,如果使用表级锁,效果上相当于为表中的所有记录都加锁,所以性能比较差,行级锁粒度细,可以实现更精准的并发控制,但是占用资源较多。

1. InnoDB中的表级锁

  • 表级别的S锁、X锁

在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的S锁或者X锁的。
另外,在对某个表执行一些诸如ALTER TABLE、DROP TABLE的DDL语句时,其他事务在对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE等语句时,会发生阻塞。同理,某个事务在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL语句也会发生阻塞。这个过程其实是通过在server层使用一种称为元数据锁(Metadata Lock,MDL)的东西来实现的,一般情况下也不会使用InnoDB存储引擎自己提供的表级别的S锁和X锁。

其实,InnoDB存储引擎的表级S锁或者X锁相当 “鸡肋”,只会在一些特殊情况下(比如在系统崩溃恢复时)用到。不过我们还是可以手动获取一下,比如在系统变量autocommit=0、innodb_table_kicjs=1时,要手动获取InnoDB存储引擎提供的表t的S锁或者X锁可以按照下面这样来写语句。

  • LOCK TABLES t READ:InnoDB存储引擎会对表t加表级别的S锁。
  • LOCK TABLES t WRITE:InnoDB存储引擎会对表t加表级别的X锁。
    不过清尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处是实现了更细粒度的行级锁,关于表级别的S锁和X锁大家了解一下就罢了。
  • 表级别的IS锁、IX锁
    当对使用InnoDB存储引擎的表的某些记录加S锁之前,需要先在表级别加一个IS锁;当对使用InnoDB存储引擎的表的某些记录加X锁之前,需要先在表级别加一个IX锁。IS锁和IX锁的使命只是为了后续在家表级别的S锁和X锁时,判断表中是否有已经被加锁的记录,以避免用遍历的方式来查看表中有没有上锁的记录。
  • 表级别的AUTO-INC锁
    在使用MySQL的过程中,我们可以为表的某个列添加AUTO_INCREMENT属性,之后在插入记录时,可以不指定该列的值,系统会自动为它赋予递增的值。比如我们创建一个表。

    CREATE TABLE t (
      id INT NOT NULL AUTO_INCREMENT,
      c VARCHAR(100),
      PRIMARY KEY(id)    
    ) Engine=InnoDB CHARSET=utf8;

    由于这个表的id字段声明了AUTO_INCREMENT,也就意味着在书写插入语句时不需要为其赋值。比如下面这样:

    INSERT INTO t(c) VALUES('aa'),('bb');

    上面这条插入语句并没有为id列显式赋值,系统会自动为它赋予递增的值,效果如下:
    image.png
    系统自动给AUTO_INCREMENT修饰的列进行递增赋值的方式主要有下面两个。

    • 采用AUTO-INC锁,也就是在执行插入语句时就加一个表级别的AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值。在该语句执行结束后,再把AUTO-INC锁释放掉。这样一来,一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,从而保证一个语句中分配的递增值是连续的

    比如我们的插入语句在执行前并不确定具体要插入多少条记录(无法预计即将插入记录的数量),比如使用INSERT ...SELECT、REPLACE ...SELECT或者LOAD DATA这种插入语句,一般是使用AUTO_INC锁为AUTO_INCREMENT修饰的列生成对应的值。

    • 采用一个轻量级的锁,在为插入语句生成AUTO_INCREMENT修饰的列的值时获取这个轻量级锁,然后在生成本次插入语句需要用的AUTO_INCREMENT修饰的列的值之后,就把该轻量级锁释放掉,而不需要等到整个插入语句执行完后才释放锁。
贴士:需要注意的是,这个AUTO-INC锁的作用范围只是单个插入语句,在插入语句执行完成后,这个锁就被释放了。这与之前介绍的锁在事务结束时释放是不一样的。

如果我们的插入语句在执行前就可以确定具体要插入多少记录,那么一般采用轻量级锁的方式对AUTO_INCREMENT修饰的列进行赋值。这种方式可以避免锁定表,可以提升插入性能。

贴士:设计InnoDB的大叔提供了一个名为innodb_autoinc_lock_mode的系统变量,用来控制到底使用上述两种方式的哪一种来为AUTO_INCREMENT修饰的列进行赋值,当innodb_autuinc_lock_mode的值为0时,一律采用AUTO-INC锁;当innodb_autoinc_lock_mode的值为2时,一律采用轻量级锁;当innodb_autoinc_lock_mode的值为1时,两种方式混着来(也就是在插入记录的数量确定时采用轻量级锁,不确定时使用AUTO-INC锁)。不过,当innodb_autoinc_lock_mode的值为2时,可能会造成不同事务中的插入语句为AUTO_INCREMENT修饰的列生成的值是交叉的,这在有主从复制的场景中是不安全的。

2.InnoDB中的行级锁

行级锁,也称为记录锁,顾名思义就是在记录上加的锁。不过设计InnoDB的大叔很有才,一个行锁玩出了多种 "花样",也就是把行锁分成了各种类型。换句话说,即使对同一条记录加行锁,如果记录的类型不同,起到的功效也是不同的。
为了故事发展,有一张表。

Create Table: CREATE TABLE `hero` (
  `number` int NOT NULL,
  `name` varchar(100),
  `country` varchar(100),
  PRIMARY KEY (`number`)
) ENGINE=InnoDB CHARSET=utf8

我们主要是想用这个表存储三国时期的英雄人物。向这个表中插入几条记录

image.png

贴士:utf8字符集没有对汉字进行排序,所以在加了首字母

这里把B+树进行了超级简化,只把聚簇索引叶子节点中的记录给拿了出来,目的是想强调聚簇索引种对记录是按照主键大小排序的。这里还略掉了聚簇索引种的隐藏列,这里明白即可。
下面看看都有哪些常用的行级锁类型。

  • Record Lock
    前面提到的记录锁就是这种类型,也就是仅仅把一条记录锁上。我们暂时叫 "正经记录锁"。这种锁类型官方名称为LOCK_REC_NOT_GAP。比如我们为number值为8的那条记录加一个正经记录锁。
    正经记录锁是有S锁和X锁之分的,我们分别称S型正经记录锁和X型正经记录锁。当一个事务获取了一条记录的S型正经记录锁后,其他事务也可以继续获取该记录的S型正经记录锁。当一个事务获取了一条记录的X型正经记录锁后,其他事务既不可以继续获取改记录的S型正经记录锁,也不可以继续获取X型正经记录锁。
  • Gap Lock
    MySQL在REPEATABLE READ隔离级别下是可以在很大程度上解决幻读现象的。解决方案有两种:使用MVCC方案解决;使用加锁方案解决。但是在使用加锁方案解决时又个大问题,就是事务在第一次执行读取操作时,那些幻影记录上不存在,我们无法给这些幻影记录加上正经记录锁。不过这难不倒设计InnoDB的大叔,它们提出了一种称为Gap Lock的锁。这种锁类型的官方名称为LOCK_GAP,也可以简称为gap锁。比如我们为number值为8的那条记录加一个gap锁。

。。。

为number值为8的记录加了gap锁,这意味着不允许别的事务在number值为8的记录的前面的缝隙插入新记录,其实就是number列的值在区间(3,8)的新记录是不允许立即插入的。比如有另一个事务想插入一条number为4的新记录,首先要定位到该条记录的下一条记录,也就是number值为8的记录,而这条记录上又有一个gap锁,所以就会阻塞插入操作;直到拥有这个gap锁的事务提交了之后将该gap锁释放掉,其他事务才可以插入number列的值在区间(3,8)中的新记录。

这个gap锁的提出仅仅是为了防止插入幻影记录而提出的。虽然gap锁有共享gap锁和独占gap锁这样的说法,但是他们起到的作用都是相同的。而且如果对一条记录加了gap锁(无论是共享还是独占的gap锁),并不会限制其他事务对这条记录加正经记录锁或者继续加其他gap锁。在强调一遍,gap锁的作用仅仅是为了防止插入幻影记录而已。

不知道大家是否发现了一个问题:给一条记录加gap锁只是不允许其他事务像这条记录前面的间隙插入新记录;那对于最后一条记录之后的间隙,也就是hero表中number值为20的记录之后的间隙该咋办呢?也就是说,给哪条记录加gap锁才能阻止其他事务插入number值在区间(20,+)的新记录呢?这时候应该想起数据页中那两条伪记录了。

  • Infimum记录:表示该页面中最小的记录。
  • Supremum记录:表示该页面中最大的记录。

为了阻止其他事务插入number值在区间(20,+)的新记录,我们可以给索引中最后一条记录(也就是number值为20的那条记录)所在页面的supremum记录上加一个gap锁。

这样就可以阻止其他事务插入number值在区间(20,+)的新记录。

  • Next-Key Lock
    正经记录锁和一个gap锁的合体,既能保护这条记录,又能阻止别的事务将新记录插入到被保护记录前面的间隙中。
  • Insert Intention Lock
    插入意向锁

Zeran
32 声望4 粉丝

学而不思则罔,思而不学则殆。