文章目录
理解事务
事务保证了数据库将从一种一致性状态转移到另一种一致性状态,事务一般对于一组操作,这组操作的执行是原子的,要么全部完成要么全部失败,而且一旦提交必然能够保证对数据库永久性的改变,即使数据库发生故障也可以恢复数据。而且事务之间应该是独立。
事务具体体现在ACID四个特性。
四个特性及innoDB的实现
【1】atomic原子性:一个事务中的所有操作都是一个不可分割的工作单元,要么全部成功,要么全部失败。这个工作单元中的任何一个操作失败,所有已经执行的操作都应该被撤销。
【2】consistency一致性:事务开始前和结束后,数据库的完整性不被破坏。事务将数据库从一个一致性转变为下一种一致性状态,因此事务是一致性的单位。
【3】isolation隔离性:事务提交前对其他事务不可见,一个事务的执行不受到其他事务的影响。
【4】durability持久性:事务一旦提交,就是永久性的改变,即使系统故障也可以恢复,持久性保证事务系统的高可靠性。
原子性
原子性是基于 undo log 和 锁 实现的。锁本质上就是数据行上的一个变量,如果其他事务发现当前此记录被上锁便不能继续对其上锁(这里指互斥锁),同时如果执行事务的过程中出现所谓,便可以通过undo log回滚到事务开始之前的一致性状态。
持久性
持久性记录redo log实现。一旦事务提交,便会刷新重做日志,重做日志记录的就是对页面的具体操作,即使脏页还没有刷新到表空间而发生宕机,恢复时可以直接读取redo log进行恢复,默认情况下,事务提交时会调用fsync()将redo log buffer中的内容同步到磁盘。
redo log写入时脏页并没有同步到磁盘,但是redo log记录了对哪些页执行什么操作,因此即使内存中的脏页数据丢失就可以恢复(除非页本身收到损坏)
Redo log用于崩溃恢复。而binlog用于基于时间点的恢复,还可以用于主从复制
隔离性
隔离性侧重研究不同事务之间的互相影响(干扰的程度)。通常使用锁机制保证两个写事务之间的隔离性,通过MVCC机制保证读写事务之间的隔离性。
一致性
一致性指的是事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。
一致性是事务追求的最终目标,前面的三个性质都是为了保证数据库状态的一致性、同时还需要应用层的保证。
事务本质上是为了服务应用层而产生的。AID是手段,而C是目的。一致性就是应用系统借助AID从一个正确的状态到达另一个正确的状态。AID是数据库的特征,而C依赖于应用层、开发者。而正确的状态就是满足预定的约束的状态。
事务的开启与提交
mysql命令行的默认设置下,事务都是自动提交的(执行完SQL语句之后就会执行commit)。
【1】禁用当前会话的自动提交(1代表自动提交,0代表手动提交)
SET @@autocommit = 0;
【2】通过start transaction 或 begin 开始一个事务
还可以通过savepoint设置一个保存点,当发起回滚的时候仅仅回滚到保存点,而保存点之前的工作不受影响。
SET autocommit=0;
START TRANSACTION;
DELETE FROM account WHERE id=25;
SAVEPOINT a; -- 设置保存点
DELETE FROM account WHERE id=28;
ROLLBACK TO a; -- 回滚到保存点
在自动提交模式下,如果没有start transaction显示地开启一个事务,那么没有sql语句会被当做一个事务执行提交操作。而auto commit是针对连接的,修改某个连接不会影响其他连接。
权限管理语句、mysql架构修改语句、DDL语句(alter、drop、create、truncate等)会执行一个隐式的commit操作。格外注意:truncate table和delete虽然可以达到同样的效果,但是前者是不可以被回滚的。
innoDB存储引擎中可以查询关于事务的统计信息(只有显示提交的事务才会被统计)
show global status like 'com_commit'
show global status like 'com_rollback'
每秒事务请求数question per second (QPS) = com_commit + com_rollback
每秒事务处理能力transaction per second(TPS)= QPS / time
锁
数据库的锁都是基于索引实现的,如果SQL命中索引,锁住的是命中条件内的索引节点(行锁),否则锁定的是整个索引树(表锁)
innoDB行锁是通过给索引上的索引项加锁实现的,因此只有当通过索引条件检索数据时innoDB才使用行级锁,否则使用表锁。
锁的类型
从对数据结构的操作类型分类
读锁(共享锁) 针对同一份数据,多个读操作可以同时进行而不会互相影响。
写锁(排它锁) 当前写操作没有完成前,它会阻断其他写锁和读锁。
从粒度分类:表锁和行锁
MySQL常用的两种引擎MyISAM和InnoDB,MyISAM默认使用表锁,InnoDB默认使用行锁。
注意:使用InnoDB引擎,如果筛选条件里面没有索引字段,就会锁住整张表,否则的话,锁住相应的行
Mysql行锁由引擎层实现,mylsam不支持行锁。
Mysql也支持lock tables和unlock tables语句,这时服务器层实现的,和存储引擎无关
意向锁是innoDB自己加的,如果要对某个元组加锁,那么需要对上层节点加意向锁。
innoDB采用的是两阶段锁定协议。在事务执行的过程中,随时都可以执行锁定,锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放的。innoDB会根据隔离级别在需要的时候为被事务访问的数据自动加上隐式锁。
意向锁
innoDB支持多粒度锁定,运行事务在行级和表级的锁同时存在。意向锁将锁定的对象分为多个层次,意向锁表明事务希望在更细粒度上进行上锁。
假设事务A对某一行加入写锁,事务B希望对该表加入加锁。事务表加锁之前必须对表中的每一行进行遍历,这样效率很低。而有了意向锁,A加行锁之前,先申请意向锁,申请成功后再申请一行的行锁。而如果事务B希望加表锁,它发现该表存在意向锁,那么申请锁的行为就会被阻塞。
申请意向锁的行为由数据库完成,当事务申请某一行的行锁的时候,数据库会自动该表的意向锁。
隔离级别
【1】读未提交:一个事务还没提交,它做的变更能被别的事务看见
【2】读已提交:一个事务提交之后,它做的变更才可以被看见
【3】可重复读:一个事务执行过程中看到的数据,总是跟这个事务启动时看到的数据一致。
【4】串行化:访问一个记录会加读写锁,出现冲突时,后访问的事务必须等前一个事务执行完成才能继续执行
注意:隔离性是事务之间的特性,同一个事务update后跟select,select查到的更新到的数据,因为二者是同一事务,不存在隔离性,而其他事务的select可能差不到
mysql的默认隔离级别是可重复读RR,而oracle的默认隔离级别是读已提交RC。其中隔离级别越高越符合数据库隔离性的规范,但是并发性能会下降。(隔离级别越低,事务请求的锁锁越少、保持锁的时间越短)。
标准的SQL下,会存在脏读、不可重复读、幻读三种问题。其中串行化可以完全避免这些问题,可重复读具有幻读问题,读已提交具有不可重复读、幻读问题,读未提交则有以上所以问题。
但是mysql通过next-key lock避免的幻读问题(指全程当前读)。
为什么mysql使用可重复读作为默认隔离级别?
这是具有历史原因的,mysql 的binlog在statement格式下,会出现主从不一致的情况。如果修改为row格式才会解决这个问题(但是mysql5.1才引入row格式)
binlog记录的顺序可能与实际的顺序不一样,导致主从数据库数据不一致。
第一个事务先执行,未结束时执行第二条事务,第二条事务先结束执行,第一个事务后结束执行。执行顺序是1-2,但是记录顺序是2-1。(如果从服务器重放这个命令,那么相当于第二条事务先开启事务并执行,然后才是第一个事务再执行)
隔离级别选择和死锁
除非在特殊的业务场景,要求一定的一致性,否则推荐读已提交。
因为RR通过间隙锁进行实现隔离级别,使得出现死锁几率更大(共享锁粒度过大,两个事务,拿到共享锁插入时导致死锁)。RR下如果条件列未命中索引会锁住整张表,而RC下只锁住待修改的目标行。而且RC一致性弱于RR,并发量更高
做一个实验:
开启两个事务,首先二者全部是拿着共享锁去读,然后让其中两个事务升级为互斥锁去读。这个时候两个事务分别等待对方释放共享锁,并且尝试获取互斥锁,因此发生死锁。
其中一个事务,因为死锁会回滚,另一个事务则因为被回滚的事务放弃竞争锁而成功返回
Mysql有两种处理死锁:**【1】等待,直到超时【2】发起死锁检测(锁等待图)。**主动回滚一条事务(需要开启innoDB_deadlock_detect=on)
死锁检测原理:
构建一个以事务为顶点、锁为边的有向图,判断这个图是否存在环。
检测到死锁之后,选择其中插入更新或者删除行数最少(回滚开销更小)的事务进行回滚。通过命令show engine innodb status 查看死锁原因
innoDB死锁的避免
【1】将事务中需要申请的锁,一次性申请完毕。可以使用select … for update一次性为增删改操作的需要使用的锁一次性申请完毕。
【2】避免使用长事务,将大事务拆分为小事务,每个事务都应该是“短小精悍”的。
【3】尽量使用(在视图一致性要求不严格,看重性能)可重复读RC级别。因为可重复读的next-key lock的粒度更大,容易造成死锁。
【4】设置锁的超时时间——innoDB_lock_wait_timeout。
【5】尽量使用主键更新,锁定程度可以退化为行锁,粒度更小。而且避免回表,提升性能。
【6】多个程序尽量约定以相同的顺序访问表,例如事务统一先修改字段A再修改字段B,如果事务1先修改A或修改B,而事务2先修改B再修改A就可能造成死锁。
锁问题
脏读
脏读就是读到了其他事务没有提交的数据,读未提交的隔离级别下具有该问题。
不可重复读
不可重复读:由于其他事务的更新操作导致,某一个事务对一个字段执行了两次读,但是读出的值不一样,是由于读的中间该字段被某一个事务修改了——读到了另外一个事务更新的数据。特指一个事务读到另外事务中提交的update数据(对已存在数据操作导致)
幻读
幻读:由于其他事务插入新记录而当前事务不可感知导致,当前事务第一读发现表只有一条记录,于是预插入一条id为2的记录,但是插入时却发现id冲突,第一次读好像出现了幻觉。(由于是可重复读的问题之一,出错最好通过更新操作体现)特指一个事务读到另外事务中提交的insert数据(对不存在数据进行操作导致)
解决不可重复读的思路,就是将每次满足添加的语句加锁,这样别的事务就不能操作where = target。而幻读不能使用该思路,因为幻读不存在“当前行”,next-key lock的思路就是限制对范围的访问(别的事务向往范围插入或更新都会被阻塞),这样就不能插入到某一个范围了。
行锁只能锁住行,但是无法锁住范围,next-key lock只有在可重复读隔离级别下才会生效。
快照读(一致性非锁定读)
innoDB存储引擎通过多版本并发控制MVCC的方式读取当前执行时间数据库中行的数据。快照读避免了读写事务之间的冲突。
某一个读事务请求读取数据库中的行数据,如果读取的行正在执行修改操作(被别的事务上了互斥锁),则读事务不会被阻塞。而是读取行的快照数据。快照数据就是当前行数据的历史版本,每行记录可能有多个历史版本,对于多版本的并发控制就是MVCC多版本并发控制。
MVCC是并发控制的一种理念,维持一个数据行的多个版本,主要用于更好的解决读写冲突(通过维护多个版本,每个版本都可以被不同的事务读,通过事务id标识,每修改一个版本就生成一个新版本),可以提高数据库并发性能。是一种解决读写冲突(非阻塞并发读)的无锁机制
快照读是默认的读取方式,不会占用和等待表上的锁,不同隔离级别下,读取的方式是不一样的。快照就是当前行的历史版本记录,每行的记录可能有多个版本。
在读已提交下,非锁定一致读总是读取被锁定行的最新快照数据。因为每次快照读都会生成一份最新的读视图。
而可重复读下,总是读取事务开始时的行快照数据。同一个事务中的第一个快照读才会创建读视图,之后的快照读获取的都是同一个读视图。
begin或者start transaction。他们不是一个事务的起点,执行到他们之后的第一个操作innoDB的语句,事务才真正启动,一致性视图是在执行第一个快照读语句时创建的start transaction with consistent snapshot命令:马上启动一个事务,一致性视图是在执行这条命令时创建的。
其中读已提交,当前事务可以看见其他事务提交的最新数据。而可重复读保证了事务启动执行期间看到的数据必须是前后一致的。而串行化使得事务完全同步执行了(串行化不存在快照读,总是在读之前对数据行加共享锁)。
Mysql中,每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚日志可以得到前一个状态的值。
有了并发读写控制,可以实现读的时候不阻塞写,写的时候不阻塞读。MVCC可以提升数据库并发读写性能,同时可以解决脏读、幻读、不可重复读问题。但是不可以解决写写造成的更新丢失问题(属于写写冲突问题,必须加锁)——MVCC可以用于读写冲突,加锁可以解决写写冲突
当前读(一致性锁定读)
加锁读、insert、update、delete(增删改本身具有查询的语义)都属于当前读,读取的是记录的最新版本,同时还要保证其他并发事务不可以修改当前记录,会对读取的记录进行加锁(悲观锁的体现)
默认下的select采用快照读,前提是隔离级别不是串行化的。快照读的实现基于MVCC,属于非阻塞读,体现了乐观锁
innoDB支持两种一致性锁定读(当前读):
【1】select…for update
对行记录加一个排他锁,其他事务不能对已锁定行加任何锁
【2】select…lock in share mode
对行记录加一个共享锁,其他事务只能对被锁定行加共享锁
补充:
innoDB通过两种方式避免死锁:
【1】超时时间设置。一旦事件超过阈值(innoDB_lock_wait_timeout)则会进行回滚。
【2】等待图wait-for-graph主动检查死锁。一旦发现等待图发生回路,则回滚undo量最小的事务。
乐观锁
为数据增加一个版本标识字段version可以实现乐观锁。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
update goods set store=store-1,version=version+1
where id=xx and version=orginal_version
乐观锁机制避免了长事务中的数据库加锁开销(两个用户操作过程中,都没有对数据
库数据加锁),大大提升了大并发量下的系统整体性能表现。
mybatis-plus提供了乐观锁支持,可以自动为实体生成乐观锁版本,并且自动进行对比
悲观锁则是使用数据库提供的锁机制,交给数据库实现。
锁的算法
innoDB有三种行锁的算法:
【1】record lock 记录锁,锁住某条记录
【2】gap lock 间隙锁,锁住某个范围,但是不包含记录本身。可以看作开区间( )
【3】next-key lock ,它的效果等同于间隙锁加上记录锁。锁住一个范围,包含行本身,可以看作左闭右开的区间[ )
例如存在一个索引,她在数据行中有三个值 1 3 5 ,那么可以分为三个范围(无穷,1]、(1,3],(3,5]、(5,无穷],某一事务执行where a = 5 lock in share mode ,此时就为表加入间隙锁,其他事务不能向间隙插入数据,这防止了写事务-写事务、或当前读事务-写事务的幻读问题发生。
(上面的数据行基于非唯一索引,因为唯一索引上的锁将退化为行锁。如果一个SQL加锁涉及多个列,那么需要组合讨论)
(默认情况下,可重复读隔离级别)
【1】当where的条件不是索引字段的话(索引未命中),innoDB会为整张表上锁,然后放到server层,在server过滤掉不符合条件的数据后,在进行解锁。
【2】当where的条件是普通索引字段的时候会使用gap lock或者next-key lock。
一般是next-key lock,不够在某些情况会退化为gap lock。例如现在有个字段age建立的普通索引,数据 10 17 20 ,现在我要查询where age = 18 ,由于向右搜索不到记录,因此退化为间隙锁,锁住(17,20)。(相当于通过优化,降低了锁的粒度,仅保存两次where age = 18 不会存在幻读问题即可)
【3】当where的条件是主键索引字段时,锁策略降级为行锁。(因为主键索引必须是唯一的,因此不会有一个相同值的主键字段被插入,不存在幻读问题)
加锁原则:访问到的对象才会被加锁
优化:如果是索引上的等值连接,给唯一索引加锁的时候,退化为行锁。向右遍历时且最后一个不满足等值条件的时候,退化为间隙锁。(如果使用范围查询,不论是什么索引类型,都会加next-key lock)
总结:
可重复读下,当前读采用next-key lock实现语义,而快照读采用MVCC(同一快照视图)实现语义。读已提交中,当前读则采用记录锁 record lock 实现语义,快照读采用MVCC(多个快照视图)实现语义。
简单总结一下什么时候加next-key lock:首先innoDB在可重复读的隔离级别下,非唯一索引的等值查询(且where的条件需要命中记录,否则退化为间隙锁),或者是范围查询,一直加到不满足条件的第一条记录为止。
优化建议:
【1】尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁。
【2】尽可能减少检索条件,避免间隙锁
【3】尽量控制事务大小,减少锁定资源量和时间长度
【4】锁住某行后,尽量不要去调别的行或表,赶紧处理被锁住的行然后释放掉锁。
【5】涉及相同表的事务,对于调用表的顺序尽量保持一致。
【6】在业务环境允许的情况下,尽可能低级别事务隔离
多版本并发控制MVCC
MVCC是一种解决读写冲突的无锁并发控制机制
每一个启动的事务都会被分配一个递增的事务ID,每个记录行都有一些隐含字段:创建了该行的事务ID、最近修改了该行的事务的ID、回滚指针、删除标志。
每当某个事务需要更新某个字段时,需要先对该行加排他锁,然后将旧版本记录拷贝到undo log中,修改记录的同时,将行的隐藏字段更新为当前事务的ID,并更新回滚指针(链表结构)。事务提交后是否锁。不同事务或者相同事务对同一记录的修改,会使得该记录形成一条关于版本的链表,链头指向最新记录,链尾指向最近的版本。
(注意,可重复读下,如果并发量很大,事务为了搜寻某个版本的数据可能要遍历链表很长时间)
当一个事务进行快照读的时候,innoDB为该事务提供一个事务ID数组(版本号数组),其中对应的事务都是数组创建时,开启事务但是没有提交事务的事务ID(活跃事务ID),这个数组就是一个逻辑上的读视图。
这个数组中的最大的事务ID和最小的事务ID可以可以划分为三个区间,(负无穷,min)对应已提交事务、[min,max]对应未提交事务,(max,正无穷)对应未开始事务 。每当事务拿到一个行记录首先判断它的事务ID,然后和事务ID数组(读视图)进行比较,如果落在低水位区间、等于当前事务的ID以及在中间数组范围内但不存在,那么说明该行记录对当前事务是可见的,否则说明不可见,此时事务顺着回滚指针向前遍历,继续执行以上的判断,直到找到一个可见的版本为止。
低于低水位数组,一定是提交过了,如果是事务自己的版本也是一定可见的,还有一种情况,数据行上的事务id属于[min,max]的范围,但是数组中找不到这个ID,这种情况也是可见的,因为如果能在数组中找到对应的ID一定是不可见的,而如果范围在[min,max]但是没有对应ID,说明这个事务创建晚于min,但是在当前事务生成视图之前已经完成了提交,因此可见。
每行数据都有多个版本,每当被某一个事务更新,就会生成一个新的版本,并使用事务id进行标记。通过回滚日志和当前版本可以计算出任何一个旧版本。
为了防止更新丢失,更新操作必须基于最新版本,而读操作可以读取旧版本(可重复读可以读旧版本,但是读已提交读的是提交的新版本,读到的一定是已经提交过的数据,否则就成脏读了)
更新数据的时候不能在旧版本上进行,否则会造成写写冲突,从而引发其他写事务的更新丢弃。而且更新涉及两阶段锁协议,如果其他事务没有释放排他锁,那么当前写事务阻塞。
可重复读在事务开始后,第一次执行select前创建readView,直到事务提交之前都不再创建。而读已提交每次select之前都会重新产生一个readView。
MVCC只在repeatable read和read committed两个隔离级别下工作。其他两个隔离级别和MVCC不兼容。因为READ UNCOMMITTED 总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE 则会对所有读取的行都加(共享)锁
MVCC的缺点就是要求每行记录包含更多的用于支持MVCC的字段,一些undo log为了支持MVCC也不能立即被回收,总体上增加了维护的开销。
可重复读下,能够避免幻读吗?
对应这个问题,应该分类讨论:
【1】其中一个事务全程快照读,两次读的中间另一个事务视图插入一行记录
【2】其中一个事务全程当前读,两次读的中间另一个事务视图插入一行记录
【3】其中一个事务先快照读再当前读,两次读的中间另一个事务视图插入一行记录
(先当前读再快照读同两次当前读,因为当前读的时候已经获取到锁了,第二次读旧不会发生冲突了)
现在假设一个场景,有两个事务A和B,一张表,里面有一个普通索引子弹num,num此时有1,2,3,4 共五条数据。A先执行一个读操作(where num<=2),然后B事务尝试插入一条num=0的记录(提交事务),这时A再次读取(where num<=2),会发生什么?
全程当前读
如果A是当前读,那么会为表加上间隙锁,B执行事务时会阻塞。当A两次读完毕后提交事务,B才会返回,最终插入成功。而且A的两次读都是一致的内容。
因此:全程当前读,基于next-key lock保证可重复读的隔离级别。
全程快照读
如果A的两次都是快照读,都会在同一个一致性视图进行读数据,和B的插入操作互不干涉(B的插入操作最终会创建一个新的版本,而对旧版本不干涉)。
因此:全程快照读,基于MVCC机制保证可重复读的隔离级别。
先快照读,再当前读
A事务
start TRANSACTION
select num
from nums where num<2
select num
from nums where num<2
for UPDATE
COMMIT
B事务
insert into nums VALUE(-2);
A第一次读到的数据和第二次读到的数据并不相同,因此先快照再当前读并不能避免幻读发生。因为快照读中,读的是旧版本而再次当前读则相当于退化为了读已提交状态。
间隙锁不能解决幻读问题,不要再误导人了
看了不少的博客,前后矛盾,我先单独谈一谈这个问题
间隙锁锁住的是间隙,如果我有一个字段age建立的普通索引10 15 17 20 ,那么我进行一个当前读加上的都是一些列开区间锁,其中幻读问题强调的是插入。这时10 15 17 20等行都是没有加锁的,如果我插入10 、15等,我再进行一次当前读将会读出一些新的记录。
而next-key lock 是间隙锁和行锁的组合,是左开右必的区间如(25,17] 、(17,20] ,是能够在当前读的时候锁住区间以及值的,可以避免幻读(这里仅指两次当前读)。
个人理解:mysql的读已提交在当前读这块是使用行锁保证的,可以避免脏读,但是存在不可重复读和幻读问题。mysql的可重复读采用next-key lock保证(在行锁继承上锁住间隙),可以避免脏读、不可重复读以及幻读(全程当前读层面)
间隙锁应该不是单独作为一个锁去使用的,如果单独作为一个锁去实现,一个事务对一个数据行进行修改时没有行锁加持,那岂不是脏读都不能避免?
next-key lock锁定范围个人总结
next-key lock只在可重复读隔离级别下使用。为主键字段(具有唯一属性的索引字段)加锁时,如果命中则退化为行锁,否则会在主键值所在间隙加gap lock(例如数据库中只有1和3,但是申请对2的锁,那么使用(1,3)的范围锁,防止2的插入)。范围查询的时候会加多个next-key lock。(如果是普通索引加锁,首先仍会对主键进行加锁)
next-key lock说白了,就是范围锁+行锁。(next-key指的就是范围的右区间是一个闭区间,对应一个行锁)。行锁可以用来阻塞对“数据行”的锁请求,范围锁用于“阻止多个事务将记录插入到同一范围”
当向右遍历到最后一个不满足等值条件(强调:等值)的时候,next-key lock会退化为间隙锁。
上图中,(数据库中是5 10 15 20)其中c>=10 是等值条件而c<11不算等值条件,因此最终加锁(5,10]和(10,15]
如果一个SQL涉及多个字段,例如a是主键,b是辅助索引等,我们需要依次讨论每个字段需要如何申请锁。(锁是基于索引实现的,一个字段可能属于多个索引,因此一个SQL的阻塞可能有多种锁定的原因)
其中a是主键索引,b是普通索引。我们只关注索引b的情况。
现在A事务执行SQL SELECT * from z where b = 3 for UPDATE;它的加锁范围是什么?
经过实验,加锁范围: 1 2 3 4 5 。其中0 6 7 8 … 的插入都是不被阻塞的。这里的实验结果还是有些出入的,目前博主也没有想到最优的解释。暂时的个人理解:行锁可以防止update,而insert具有“先查询再插入”的语义,因此需要对“前一个间隙”加间隙锁,而后一个加间隙进行加锁可能是由于实现的问题(bug?)。(至于为什么前一个间隙的左边界也被上锁,这个有待讨论)