当前位置: 首页 > 科技观察

字节的第二面,在执行两个事务之间的SQL语句的过程中,造成了死锁

时间:2023-03-16 22:55:24 科技观察

大家好,我是小林。之前接到一个读者采访字节的时候,被问到一个关于MySQL的问题。如果熟悉MySQL的锁机制,应该一眼就能看出会发生死锁。但是加了什么样的锁会导致死锁,需要我们具体分析。下面说一下上面两个事务中SQL语句执行过程中加了哪些锁,导致了死锁。准备工作首先创建一张t_student表,假设除了id字段,其他字段都是普通字段。CREATETABLE`t_student`(`id`intNOTNULL,`no`varchar(255)DEFAULTNULL,`name`varchar(255)DEFAULTNULL,`age`intDEFAULTNULL,`score`intDEFAULTNULL,PRIMARYKEY(`id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;然后,插入相关数据后,t_student表中的记录如下:开始实验实验开始前请说明实验环境:MySQL版本:8.0.26隔离级别:可重复读(RR)启动两个事务。根据题目的SQL执行顺序,流程如下:可以看出,事务A和事务B在执行insert语句后都处于等待状态(前提是没有开启死锁检测),即,因为他们都在等待对方释放锁,所以发生了死锁。为什么会发生死锁?我们可以使用select*fromperformance_schema.data_locks\G;语句查看事务执行SQL时加了哪些锁。接下来分析每条SQL语句具体加了哪些锁。时间1阶段锁分析时间1阶段,事务A执行如下语句:#transactionAmysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>updatet_studentsetscore=100whereid=25;QueryOK,0受影响的行(0.01秒)匹配的行:0更改:0警告:0然后执行select*fromperformance_schema.data_locks\G;语句查看此时事务A加了哪些锁。从上图可以看出,增加了两把锁,分别是:表锁:X型意向锁;行锁:X型间隙锁;这里重点关注行锁,图中LOCK_TYPE中的RECORD代表的是行级锁,不是记录锁,通过LOCK_MODE可以确认是next-key锁、间隙锁还是记录锁:如果LOCK_MODE是X,表示下一键锁;如果LOCK_MODE为X,REC_NOT_GAP,表示记录Lock;如果LOCK_MODE为X,GAP,表示间隙锁;因此,此时事务A在主键索引(INDEX_NAME:PRIMARY)上加了一个间隙锁,锁范围为(20,30)。Time2阶段锁定分析在Time2阶段,事务B执行如下语句:#transactionBmysql>begin;QueryOK,0rowsaffected(0.00sec)mysql>updatet_studentsetscore=100whereid=26;QueryOK,0受影响的行(0.01秒)匹配的行:0更改:0警告:0然后执行select*fromperformance_schema.data_locks\G;语句查看此时事务B加了哪些锁。从上图可以看出,一共加了两把锁,分别是:表锁:X类型的意向锁;行锁:X类型的间隙锁;因此,此时事务B在主键索引(INDEX_NAME:PRIMARY)上加了一个间隙锁,锁范围为(20,30)。事务A和事务B的间隙锁范围相同,为什么不冲突?两个事务的间隙锁相互兼容,不会冲突。MySQL官网上还有一个非常挑剔的描述:InnoDB中的间隙锁是“纯抑制性的”,也就是说它们的唯一目的就是防止其他事务插入到间隙中。间隙锁可以共存。一个事务获取的间隙锁不会阻止另一个事务在同一间隙上获取间隙锁。共享和排他间隙锁之间没有区别。它们彼此不冲突,并且它们执行相同的功能。间隙锁的意义仅在于Blockingintervals被插值,因此可以共存。一个事务获取的间隙锁不会阻止另一个事务获取相同间隙范围的间隙锁。共享和排他间隙锁之间没有区别。它们互不冲突,功能相同。时间3阶段锁分析时间3,事务A插入一条记录:#时间3阶段,事务A插入一条记录mysql>insertintot_student(id,no,name,age,score)value(25,'S0025','sony',28,90);///阻塞等待...此时,事务A处于等待状态。然后执行select*fromperformance_schema.data_locks\G;语句来查看事务A正在获取什么锁并导致它被阻塞。可以看出事务A的状态是等待(LOCK_STATUS:WAITING),因为事务B产生的间隙锁(范围(20,30))插入了一条记录,所以事务A的插入操作产生了一个插入意向锁(LOCK_MODE:INSERT_INTENTION)。什么是插入意向锁?注意!插入意向锁虽然名字中有意向锁三个字,但它并不是意向锁。它属于行级锁,是一种特殊的间隙锁。MySQL官方文档中有一个重要的描述:Insert意向锁是Insert操作在rowInsertion之前设置的一种gaplock。这个锁以这样一种方式发出插入意图的信号,即如果多个事务插入到同一索引间隙中,如果它们不在间隙内的同一位置插入,则无需等待彼此。假设有值为4和7的索引记录,分别尝试Insert值为5和6的事务,在获得排他锁之前,分别用Insert意向锁锁定4和7之间的间隙Inserted行,但不要互相阻塞,因为这些行是不冲突的。这段话说明,插入意向锁虽然是一种特殊的间隙锁,但它与间隙锁的区别在于,该锁只用于并发的插入操作。如果说间隙锁锁定的是一个范围,那么“插入意向锁”锁定的是一个点。所以从这个角度来看,插入意向锁确实是一种特殊的间隙锁。insertintentlocks和gaplock还有一个很重要的区别,就是“insertintentlocks”虽然也属于gaplocks,但是两个事务不能同时存在,一个持有gaplock,另一个拥有区间内的gap。插入意向锁(当然,如果不在gaplock区间内,也可以插入意向锁)。所以插入意向锁和间隙锁是有冲突的。另外补充一点,插入意向锁的时机是:每插入一条新记录,都要检查要插入的记录的下一条记录是否加了间隙锁。如果加了间隙锁,Insert语句就会被阻塞。阻止并生成插入意向锁。时间4阶段锁定分析时间4,事务B插入一条记录:#时间4阶段,事务B插入一条记录mysql>insertintot_student(id,no,name,age,score)value(26,'S0026','ace',28,90);///阻塞等待...此时,事务B处于等待状态。然后执行select*fromperformance_schema.data_locks\G;语句来查看事务B正在获取什么锁并导致它被阻塞。可以看出,事务B在生成insertintent锁时被阻塞了,因为事务B向事务A生成的间隙锁(范围(20,30))插入了一条记录,而insertintent锁和间隙锁是冲突的,所以事务B在获取插入意向锁的时候就陷入了等待状态。最后回答一下,为什么会出现死锁?这种情况下,事务A和事务B在update语句执行后都持有范围为(20,30)的间隙锁,接下来的insert操作等待对方事务获取insert意向锁。间隙锁被释放,导致循环等待,满足了死锁的四个条件:互斥、占有等待、不占有、循环等待,所以发生死锁。综上所述,即使两个事务产生的间隙锁范围相同,也不会有冲突,因为间隙锁的目的是为了防止其他事务插入数据,所以间隙锁和间隙锁是相互兼容的.在执行insert语句时,如果插入的记录在其他事务持有的间隙锁范围内,则insert语句会被阻塞,因为当insert语句遇到间隙锁时,会产生insertintentlock,然后insert意向锁和间隙锁是互斥的。如果两个事务向对方持有的间隙锁范围内插入一条记录,并且插入操作在等待对方事务的间隙锁释放,以获得插入意向锁,则循环等待造成的,满足了死锁的要求四个条件:互斥、占有等待、不占有、循环等待,所以产生了死锁。