InnoDB存储引擎默认的隔离级别是可重复读,MVCC多版本并发控制只解决快照读情况下的数据隔离,而对于当前读,InnoDB使用锁进行并发控制。InnoDB锁这篇文章主要参考了MySQL官方文档,并在上面加了一些自己的理解。如果有兴趣阅读英文,也可以阅读MySQL官方文档。本文分为以下章节:共享锁与排它锁;意向锁;行锁;间隙锁;Next-Key锁插入意向锁;自增锁;分为两类:共享锁(S)和排它锁(X)。共享锁:对一行数据持有共享锁的事务,可以读取该行锁对应的行中的数据;独占锁:事务对一行数据持有独占锁,可以修改该行锁对应的行中的数据;如果事务T1持有R行的共享锁,那么事务T2对R行的访问分为两种情况:如果事务T2请求R行的共享锁,则事务T2可以请求成功,请求完成后,事务T1和事务T2同时持有行R的共享锁;如果事务T2请求行R的排它锁,事务T2将被阻塞,直到事务T1释放锁或事务超时回滚;如果事务T1持有R行的共享锁,那么不管事务T2对R行的请求,共享锁和排他锁都会被事务T1阻塞,直到事务T1释放锁或者事务T2回滚。意向锁InnoDB支持多粒度的锁。例如下面两条SQL语句,锁定的对象是完全不同的:SELECT*FROMUSER_INFOWHEREID=1FORUPDATE,其中ID为主键,ID=1的数据行存在,那么这条SQL会获取ID=1的数据行的独占锁;LOCKTABLESUSER_INFOWRITE,其中存在USER_INFO表,则此SQL会获取USER_INFO表的独占锁;表锁和行锁之间也存在互斥某些情况下,比如表上的排他锁和表中每行数据的排他锁冲突(当然表中的内容是不允许的锁表后才能修改),这种互斥如何实现?InnoDB使用意向锁来实现表锁和行锁的互斥。意向锁是表级锁。当对一行数据加排他锁或独占锁时,会先给该数据行所在的表加一个意向锁。意向锁分为两种:共享意向锁:一个事务会对表中的一行数据加一个共享锁;独占意向锁:一个事务会对表中的一行数据加一个独占锁;所以给表加意向锁的情况也分为两种:如果一个事务需要获取一行数据的共享锁,则必须先获取数据所在表的共享意向锁,比如SQL语句SELECT*FROMUSER_INFOWHEREID=1LOCKINSHAREMODE会先给表USER_INFO加一个共享意向锁锁;如果事务需要获取一行数据的排他锁,则必须先获取该数据所在表的意向排他锁。例如SQL语句SELECT*FROMUSER_INFOWHEREID=1FORUPDATE会先给USER_INFO表加一个共享排他锁;表锁和意向锁的冲突情况如下:表排它锁共享排它锁表共享锁共享意向锁表排它锁冲突冲突共享排它锁冲突不冲突冲突不冲突表共享锁冲突不冲突不冲突conflict共享意向锁冲突无冲突无冲突无冲突如果事务请求的表锁与表上已有的锁没有冲突,则事务可以成功请求锁;如果事务请求的锁与表上已有的锁冲突,则事务必须等待表锁被释放,否则需要回滚当前事务。我们可以注意到意向锁之间没有互斥关系,因为意向锁代表修改表中的一行数据,两个意向锁代表修改表中的两行数据,所以两个意向锁不一定冲突。意向锁只会和表锁发生冲突,比如LOCKTABLESUSER_INFOWRITE会给表加表锁。在InnoDB中,我们可以通过SHOWENGINEINNODBSTATUS语句查看表锁状态。下面是锁状态的例子:TABLELOCKtable`test`.`t`trxid10080锁模式IX行锁行锁是加在索引上的锁。例如SELECT*FROMUSER_INFOWHEREID=1FORUPDATE,如果ID是唯一索引,则SQL语句会在ID对应的索引节点上加排它锁,防止其他事务修改行数据。行锁加的对象就是索引节点。如果表没有定义索引,InnoDB将创建一个隐藏的聚簇索引并使用该索引添加行锁。在InnoDB中,我们可以通过SHOWENGINEINNODBSTATUS语句查看行锁状态。下面是锁状态的例子:RECORDLOCKSspaceid58pageno3nbits72index`PRIMARY`oftable`test`.`t`trxid10078lock_modeXlocksrecbutnotgapRecordlock,heapno2PHYSICAL记录:n_fields3;紧凑格式;信息位00:len4;十六进制8000000a;升序;;1:长度6;十六进制00000000274f;升序'O;;2:长度7;十六进制b60000019d0110;升序;;GaplockGaplock锁住索引之间的间隙,比如SQL语句SELECT*FROMUSER_INFOWHEREID>10andID<20FORUPDATE,事务会向数据库发送ID到10之间的所有节点间隙加间隙锁索引树上有20个。当另一个事务试图向数据库中插入ID=15时,它将被间隙锁阻塞。间隙锁中的间隙可以包含多个索引节点、单个索引阶段或根本没有节点。间隙锁主要用于解决可重复读隔离级别下的幻读问题。对于唯一索引,如果使用等价查询,间隙锁就会退化为行锁。在下面的SQL中,ID为唯一索引列,存在ID=100的数据,那么下面的SQL只会加行锁:SELECT*FROMchildWHEREid=100;如果ID不是唯一索引,那么上面的SQL语句会在Id=100和ID索引树中的前一个节点之间的间隙上加一个GAP锁。间隙锁之间没有冲突,两个间隙锁之间的节点删除后,两个间隙锁会合并为一个间隙锁。InnoDB中的间隙锁只有一个目的,就是防止数据被插入到间隙中。间隙锁只与插入意向锁冲突,不与其他任何锁冲突。可以通过将事务隔离级别更改为已提交读取或启用innodb_locks_unsafe_for_binlog系统变量来禁用间隙锁。当禁用间隙锁时,InnoDB也会释放不匹配行的记录锁(违反了2PL锁定原则)。对于UPDATE语句,InnoDB进行“半一致”读:读取最新提交的数据,MySQL根据最新提交的数据判断是否满足UPDATE语句中的WHERE条件。Next-KeylockNext-Keylock是rowlock和gaplock的组合。在对InnoDB唯一索引进行加锁的??过程中,InnoDB会从索引中寻找符合条件的索引节点,并对这些符合条件的索引节点加行锁。.如果给一条行记录加一个Next-Key锁而不是行锁,那么Next-Key锁不仅会给记录本身加行锁,还会在行锁之前的间隙加一个间隙锁。两种形式的组合Next-Key。Next-Key不允许其他事务向锁定的间隙中插入数据。假设ID索引包含值10、11、13、20,那么给索引在先的节点加Next-Key锁可能有以下几种情况。下文中,括号表示排除间隙,方括号表示包含端点:如果索引10位于索引11的节点上添加Next-Key,锁定范围为(负无穷大,10];如果索引11所在节点添加Next-Key,锁定范围为(10,11];索引13所在节点Key添加Next-Key,锁定范围为(11,13]);如果在索引20所在的节点添加Next-Key,则锁定范围为(13,20];如果在20之后的间隙添加Next-Key,则锁定范围为(20,正无穷大);对于最后一个gap,可以理解为:InnoDB中有一个虚拟的最大节点,Next-Key会加到这个节点上,在InnoDB中我们可以通过SHOWENGINEINNODBSTATUS语句查看Next-Key锁状态,下面是锁状态的例子:RECORDLOCKSspaceid58pageno3nbits72index`PRIMARY`oftable`test`.`t`trxid10080lock_modeXRecordlock,heapno1PHYSICALRECORD:n_fields1;紧凑格式;信息位00:len8;十六进制73757072656d756d;ascsupremum;;记录锁,堆号2物理记录:n_fields3;紧凑格式;信息位00:len4;十六进制8000000a;升序;;1:长度6;十六进制00000000274f;升序'O;;2:长度7;十六进制b60000019d0110;asc;;插入意向锁插入意向锁是一种间隙锁,在向数据库中插入一行新数据时,需要在插入间隙中加上。插入意向锁之间不冲突,比如两个事务分别打算将5和6插入到(4,7]之间的空隙中,两个事务都会在(4,7]中的空隙中加一个插入意向锁,但是两个事务互不阻塞假设有两个事务,事务A和事务B,数据库表中有90和102两条记录。事务A为ID大于100的索引记录添加Next-Key独占锁:mysql>CREATETABLEchild(idint(11)NOTNULL,PRIMARYKEY(id))ENGINE=InnoDB;mysql>INSERTINTOchild(id)values(90),(102);mysql>STARTTRANSACTION;mysql>SELECT*FROMchildWHEREid>100FORUPDATE;+-----+|编号|+-----+|102|+-----+事务B试图向数据库中插入一条101记录:mysql>STARTTRANSACTION;mysql>INSERTINTOchild(id)VALUES(101);通过SHOWENGINEINNODBSTATUS可以看到此时数据库的锁等待情况:RECORDLOCKSspaceid31pageno3nbits72index`PRIMARY`oftable`test`.`child`trxid8731lock_modeXlocksrecinsert之前的间隙waitingRecord锁,堆3PHYSICALRECORD:n_fields3;紧凑格式;信息位00:len4;十六进制80000066;上升f;;1:长度6;十六进制000000002215;asc";;2:len7;hex9000000172011c;一种特殊的表级锁,由包含AUTO_INCREAMENT的表中的事务使用。在最简单的情况下,如果一个事务正在向表中插入数据行,该事务将占用auto-increment,当数据插入表时,会被锁阻塞,我们可以通过innodb_autoinc_lock_mode变量来控制自增锁的自增算法,mysql对自增锁有很多优化,其中本文不再详细介绍。我是玉狐大神,欢迎大家关注我的微信公众号:wzm2zsd参考文档MySQL官方文档
