本文主要是让大家快速了解InnoDB中的锁相关知识。得到了。你要去商场上厕所,这个时候你会做什么?把门锁上。如果不锁门,你去厕所,手放在上面,啪的一声打开门,似乎有些不妥。数据也是如此。在并发场景下,如果不对数据加锁,会直接破坏数据的一致性,如果你的业务涉及到钱,后果就更严重了。锁的分类InnoDB中有哪几种锁?其实你应该已经知道很多了。比如在面试中,你会被问到存储引擎MyISAM和InnoDB的区别。你会说MyIASM只有表锁,而InnoDB也有表锁。支持行锁和表锁。您可能还会被问及乐观锁定和悲观锁定之间的区别是什么。锁的概念和名词很多。如果你对锁没有一个完整的世界观,你就很难理解。接下来,让我们对这些锁进行分类。根据锁的粒度可以分为:表锁行锁页锁这里不讨论,页锁是BDB(BerkeleyDB)存储引擎才有的概念,我们这里主要讨论InnoDB存储引擎。按照加锁的思想,按照加锁的思想,可以分为:悲观锁乐观锁这里的悲观和乐观术语和你平时理解的术语意思是一样的。乐观锁认为大概率不会发生冲突,只在必要的时候加锁。悲观锁认为发生冲突的概率很大,所以无论是否需要加锁都会进行加锁操作。根据兼容性,锁可以分为:共享锁独占锁一个资源上了共享锁,可以共享给他人,如果加了独占锁,其他人就拿不到锁,这种情况下就不能操作了。根据锁的实现,这里的实现是InnoDB中锁的具体类型,分别是:意向锁、记录锁、间隙锁、Next-Key锁、插入意向锁(InsertIntentionLocks)、自增锁(AUTO-INCLocks)即使按照这个分类来划分锁,看到这么多锁名词,可能还是有点眼花缭乱。比如我在SELECT...FORUPDATE的时候加了什么样的锁?我们要透过现象看本质,什么是本质?本质是给什么对象加锁,这个很好回答:加到表给行加锁的本质是什么?其实质就是给索引加锁。InnoDB中意向锁支持不同粒度的锁、行锁和表锁。例如,locktables命令将在相应的表上持有独占锁。为了让不同粒度的锁更加实用,InnoDB设计了意向锁。意向锁是一种表级锁,表示下一个事务将使用哪种类型的锁。它有以下两种类型:共享意向锁(IS)表示事务打算在表中添加记录共享锁排他意向锁(IX)是排他锁。比如select...forshare是加共享意向锁,SELECT..FORUPDATE是加排他意向锁。规则如下:如果一个事务想要获取表中某行的共享锁,则必须先获取该表的共享意向锁或排他意向锁。同样,如果要获取排他锁,必须先获取排他意向锁获取锁,但如果不兼容则无法获取锁,直到不兼容锁被释放。您看到这里可能会遇到问题,因为意向锁不会阻止除LOCKTBALES之外的任何内容。那我需要它做什么?仍然举例,假设事务A获取了student表中id=100行的共享锁,然后事务B需要申请student表的排它锁。而且这两个锁明显是冲突的,还是针对同一行的。那么InnoDB需要如何感知A已经获取了这把锁呢?是否遍历整棵B+树?不,答案是意向锁。当事务B申请排他锁写表时,InnoDB会发现事务A已经获取了该表的意向共享锁,说明student表中的记录已经被共享锁锁定。此时会被阻塞。此外,意向锁不会阻塞除LOCKTABLES之类的操作之外的任何其他操作。也就是说,意向锁只和表级锁冲突,不会和行级锁冲突。因为意向锁的主要目的是表明有人即将或正在锁定一行。就像你去图书馆找书一样,你不需要在一个个挨着的书架上找。你可以直接到服务台,用电脑搜索一下图书馆有没有这本书。记录锁这是记录锁,是行锁的一种。记录锁的锁定对象是该行数据对应的索引。如果你对索引不是很清楚,可以看这篇文章。当我们执行SELECT*FROMstudentWHEREid=1FORUPDATE语句时,会在值为1的索引上加一个记录锁,至于一个表没有索引怎么办?这个问题在上面提到的文章中也有解释。当一张表没有定义主键时,InnoDB会创建一个隐藏的RowID,并使用这个RowID创建聚合簇索引。随后的记录锁也被添加到这个隐藏的聚簇索引中。当我们启动一个事务更新id=1的数据行时,如果不立即提交事务,然后启动另一个事务更新id=1的行,我们可以使用showengineinnodbstatus查看这一次,我们可以看到lock_modeX的字样“locksrecbutnotgapwaiting”。X表示独占锁。由此我们可以看出,记录锁也可以分为共享锁和排它锁两种方式。当我们使用FORUPDATE时,它是独占的,当我们使用LOCKINSHAREMODE时,它是共享的。上面的话出现的gap是行锁的另一种实现方式。间隙锁对于间隙锁(GapLocks)来说,被锁定的对象也是一个索引。为了更好的理解间隙锁,我们举个例子。SELECTnameFROMstudentWHEREageBETWEEN18AND25FORUPDATE假设我们已经为age建立了非聚集索引,运行这条语句会阻止其他事务向student表中添加18-25的数据,不管表中是否真的有18-25岁的数据。因为间隙锁的本质是在索引上锁定一个范围,而索引在InnoDB底层B+树上的存储是有序的。另一个例子:SELECT*FROMstudentWHEREage=10FORUPDATE;值得注意的是,这里的age并不是唯一索引,而是简单的非聚集索引。这时会对age=10的数据加一个recordlock,锁住age<10的Gap。如果当前事务没有提交,其他事务要插入一条age<10的数据就会被阻塞。间隙锁是MySQL综合考虑性能和并发的折中方案,只有在**可重复读(RR)下才有,如果当前事务的隔离级别是ReadCommitted(RC)**,MySQL禁用缝隙锁。刚才说了,记录锁分为共享锁和排他锁,间隙锁其实也是一样的。但与记录锁不同的是,共享间隙锁和排他间隙锁并不互斥。到底是怎么回事?我们还是要透过现象看本质。间隙锁的目的是什么?为了防止其他事务在该Gap中插入数据的共享和排他gap锁在这个目标上是一致的,所以可以同时存在。Next-KeyLocksNext-KeyLocks(下一键锁)是InnoDB对行锁的最后一个实现。Next-KeyLocks实际上是记录锁和间隙锁的组合。也就是说,key锁会在对应的index上加一个recordlock,另外会加锁一个range。但并不是所有的钥匙锁都是这样玩的。对于以下SQL:SELECT*FROMstudentWHEREid=23;在本例中,id是主键,一个唯一索引,无论其他事务插入多少数据,id=23的数据永远只有一条。这时候加间隙锁就完全没必要了,反而会降低并发。因此,当使用的索引是唯一索引时,临时键锁将降级为记录锁。假设我们一共有10、20、30这3个索引数据,那么对应keylock,可能的锁定范围会是:(∞,10](10,20](20,30](30,∞)InnoDB默认的事务隔离级别是可重复读(RR),在这种情况下,InnoDB会使用相邻键锁来防止幻读。幻读的简单解释就是在一个事务内,你执行了两个查询,第一个queryyield5条数据,但是第二次Check,发现了7条数据,这就是幻读。可能你从之前的很多博客或者面试刻板印象中了解到,InnoDB的RR事务隔离级别可以防止幻读,RR防止幻读key是key锁,比如假设student表有两行数据,ids分别是90和110SELECT*FROMstudentWHEREid>100FORUPDATE;SQL语句执行时,InnoDB会给出区间(90,110]和(110,∞)添加间隙锁,并添加一个记录锁定到id=110的索引。这样其他事务就无法向这个区间添加数据,即使100根本不存在。InsertIntentionLocks接下来是InsertIntentionLocks,在我们执行INSERT语句之前会加的锁。它本质上是一种间隙锁。再举个例子,假设我们现在有索引记录10和20,事务A和B分别插入索引值为14和16的数据。此时事务A和B都会用插入意向锁锁定10和20之间的Gap,在获取到插入意向锁后,再获取14和16的排它锁。此时事务A和B不会因为插入的行不同而互相阻塞。自增锁最终是自增锁(AUTO-INCLocks)。自增锁的本质是表锁,比较特殊。当事务A向包含AUTO_INCREMENT列的表添加新数据时,它会持有自增锁。此时其他事务B必须等待,以保证事务A实现连续自增,中间没有间隙。
