InnoDB数据锁–第2.5部分“锁”(深刻研究)

做者:Kuba Łopuszański 译:徐轶韬mysql


如今,咱们将InnoDB数据锁-第2部分“锁”了解到的全部知识放在一块儿,进行深刻研究:sql

mysql> BEGIN;Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t FOR SHARE;+----+| id |+----+| 5 || 10 || 42 |+----+3 rows in set (0.00 sec)
mysql> DELETE FROM t WHERE id=10;Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO t VALUES (4);Query OK, 1 row affected (0.00 sec)
mysql> SELECT INDEX_NAME,LOCK_TYPE,LOCK_DATA,LOCK_MODE FROM performance_schema.data_locks WHERE OBJECT_NAME='t';+------------+-----------+------------------------+---------------+| INDEX_NAME | LOCK_TYPE | LOCK_DATA | LOCK_MODE |+------------+-----------+------------------------+---------------+| NULL | TABLE | NULL | IS || PRIMARY | RECORD | supremum pseudo-record | S || PRIMARY | RECORD | 5 | S || PRIMARY | RECORD | 10 | S || PRIMARY | RECORD | 42 | S || NULL | TABLE | NULL | IX || PRIMARY | RECORD | 10 | X,REC_NOT_GAP || PRIMARY | RECORD | 4 | S,GAP |+------------+-----------+------------------------+---------------+8 rows in set (0.00 sec)

咱们看到:
shell

  • 第一个SELECT * FROM t FOR SHARE;在五、十、42和supremum pseudo-record上建立S锁(在间隙和记录上)这意味着整个轴都被锁覆盖。而这正是所需的,能够防止任何其余事务修改此查询的结果集。一样,这须要先对表t加IS数据库

  • 接下来,DELETE FROM t WHERE id=10;首先得到的IX表锁以证实它打算修改表,而后得到的X,REC_NOT_GAP修改ID=10的记录缓存

  • 最后,INSERT INTO t VALUES (4);看到它已经具备IX,所以继续执行插入操做。这是很是棘手的操做,须要谈谈咱们已抽象的细节。首先从临时闩锁 (注意单词:“ latching”,而不是“ locking”!)开始,查看页面是不是放置记录的正确位置,而后在插入点右侧闩住锁系统队列并检查是否有*,GAPSX锁。咱们的例子中没有记录,所以咱们当即着手插入记录(它有一个隐式锁,由于它在“last modified by”字段中有咱们的事务的id,但愿这解释了为何在记录4上没有显式的X,REC_NOT_GAP锁)。相反的状况是存在一些冲突的锁,为了显式地跟踪冲突,将建立一个等待的INSERT_INTENTION锁,以便在授予操做后能够重试最后一步是在轴上插入新点会将已经存在的间隙分红两部分。对于旧间隙,已经存在的任何锁都必须继承到插入点左侧新建立的间隙。这就是咱们在第4行看到S,GAP的缘由:它是从第5行的S继承的ruby

这只是涉及到的真正复杂问题的冰山一角(咱们尚未讨论从已删除的行继承锁,二级索引,惟一性检查..),可是从中能够获得一些更深层次的想法:微信

  • 一般,要提供可串行性,您须要“锁定所见内容”,这不只包括点,并且还包括点之间的间隙。若是您能够想象查询在扫描时如何访问表,那么您大均可以猜想它将必须锁定什么。这意味着拥有良好的索引很重要,这样您就能够直接跳到要锁定的点,而没必要锁定整个扫描范围。并发

  • 反之亦然:若是您不关心可串行性,您能够尝试不锁定某些东西。例如,在READ COMMITTED隔离级别较低的状况下,咱们尝试避免锁定行之间的间隙(所以,其余事务能够在行之间插入行,这会致使所谓的“幻读”)性能

  • 在InnoDB中,全部那些“正在插入”和“正在删除”的行,实际上都存在于索引中,所以出如今轴上并将其分红多个间隙。这与某些其余引擎造成对比,其余引擎将正在进行的更改保留在“暂存区”中,而且仅在提交时将其合并。这意味着即便在概念上并发事务之间没有交互(例如,在提交事务以前,咱们不该该看到行被事务插入),但在低级别实现中,它们之间的交互仍然不少(例如,事务能够在还没有正式存在的行上有一个等待锁)。所以,看到Performance_schema.data_locks报告还没有插入或已被删除的行,不须要感到惊讶(后者将最终被清除)优化

记录锁的压缩(以及丢失的LOCK_DATA)

在上面的示例中,您看到了一个很是有用的LOCK_DATA列,该列为您显示了放置记录锁的索引列的行值。这对于分析状况很是有用,可是将“ LOCK_DATA”显式存储在内存对象中会很浪费,因此当你查询performance_schema时,这些数据其实是实时重建的。data_locks表来自锁系统内存中可用的压缩信息,它与缓冲池页面中的可用数据结合在一块儿。也就是说,锁系统根据记录<space_id, page_no>所在的页面和页面中的记录heap_no编号来标识记录锁。(这些数字一般没必要与页面上记录值的顺序相同,由于它们是由小型堆分配器分配的,在删除、插入和调整行大小时,尽可能重用页面内的空间)这种方法具备一个很好的优势,便可以使用三个固定长度的数字来描述一个点:space_id, page_no, heap_no此外,一个查询必须在同一页上锁定几行是一个常见的状况,全部锁(仅heap_no不一样)一块儿存储在一个有足够长的位图的单一对象,这样heap_no第一位能够表示给定记录是否应被此锁实例覆盖。(这里须要权衡取舍,由于即便咱们只须要锁定一条记录,咱们也会“浪费”整个位图的空间。值得庆幸的是,每页记录的数量一般足够小,您能够负担n / 8个字节)

所以,即便Performance_schema.data_locks分别报告每一个记录锁,它们一般也仅对应于同一对象中的不一样位,而且经过查看OBJECT_INSTANCE_BEGIN列能够看到:

> CREATE TABLE t(id INT PRIMARY KEY);> insert into t values (1),(2),(3),(4);> delete * from t where id=3;> insert into t values (5);> BEGIN;> SELECT * FROM t FOR SHARE;+----+| id |+----+| 1 || 2 || 4 || 5 |+----+> SELECT OBJECT_INSTANCE_BEGIN,INDEX_NAME,LOCK_TYPE,LOCK_DATA,LOCK_MODE  FROM performance_schema.data_locks WHERE OBJECT_NAME='t';+-----------------------+------------+-----------+------------------------+-----------+| OBJECT_INSTANCE_BEGIN | INDEX_NAME | LOCK_TYPE | LOCK_DATA | LOCK_MODE |+-----------------------+------------+-----------+------------------------+-----------+| 3011491641928 | NULL | TABLE | NULL | IS || 3011491639016 | PRIMARY | RECORD | supremum pseudo-record | S || 3011491639016 | PRIMARY | RECORD | 1 | S || 3011491639016 | PRIMARY | RECORD | 2 | S || 3011491639016 | PRIMARY | RECORD | 5 | S || 3011491639016 | PRIMARY | RECORD | 4 | S |+-----------------------+------------+-----------+------------------------+-----------+


请注意,SELECT..FROM t..返回的行以其语义顺序(以id递增)表示,这意味着扫描主索引的最简单方法其实是以主键的顺序访问行,由于它们在页面堆中造成了一个链表。可是,SELECT..from performance_schema.data_locks揭示了内部实现的一些提示:id = 5的新插入行进入了id = 3的已删除行留下的空缺。咱们看到全部记录锁都存储在同一个对象实例中,而且咱们能够猜想,这个实例的位图为heap_no设置了与全部实际行和最高伪记录对应的位

如今,让咱们证实锁系统并不真正知道列的值,所以咱们必须查看缓冲池中实际页的内容以填充LOCK_DATA列。能够将缓冲池视为磁盘上实际页面的缓存(抱歉,过于简化:实际上,它可能比磁盘页面上的数据更新,由于它还包含存储在重作日志增量中的页补丁)Performance_schema仅使用来自缓冲池的数据,而不使用来自磁盘的数据,若是它没法在其中找到页面,不会尝试从磁盘获取数据,而是在LOCK_DATA列中报告NULL。咱们如何强制从缓冲池中逐出页?总的来讲:我不知道。彷佛可行的方法是将更多的新页推入缓冲池以达到其容量,而且逐出最先的页。为此,我将打开一个新客户端并建立一个表,使其太大而没法容纳在缓冲池中。有多大?

con2> SELECT @@innodb_buffer_pool_size;+---------------------------+| @@innodb_buffer_pool_size |+---------------------------+| 134217728 |+---------------------------+


好的,咱们须要推送128MB的数据。(能够经过将缓冲池的大小调整为较小的值来简化此实验,一般能够动态地进行此操做,不幸的是,“块”的默认大小很大,以致于不管如何咱们都没法将其减少到128MB如下)

con2> CREATE TABLE big( id INT PRIMARY KEY AUTO_INCREMENT, blah_blah CHAR(200) NOT NULL );con2> INSERT INTO big VALUES (1,REPEAT('a',200));con2> INSERT INTO big (blah_blah) SELECT blah_blah FROM big;con2> INSERT INTO big (blah_blah) SELECT blah_blah FROM big;con2> INSERT INTO big (blah_blah) SELECT blah_blah FROM big;...con2> INSERT INTO big (blah_blah) SELECT blah_blah FROM big;Query OK, 262144 rows affected (49.14 sec)Records: 262144 Duplicates: 0 Warnings: 0

..就足够了。让咱们再次查看performance_schema.data_locks:

> SELECT OBJECT_INSTANCE_BEGIN,INDEX_NAME,LOCK_TYPE,LOCK_DATA,LOCK_MODE FROM performance_schema.data_locks WHERE OBJECT_NAME='t';+-----------------------+------------+-----------+------------------------+-----------+| OBJECT_INSTANCE_BEGIN | INDEX_NAME | LOCK_TYPE | LOCK_DATA | LOCK_MODE |+-----------------------+------------+-----------+------------------------+-----------+| 3011491641928 | NULL | TABLE | NULL | IS || 3011491639016 | PRIMARY | RECORD | supremum pseudo-record | S || 3011491639016 | PRIMARY | RECORD | NULL | S || 3011491639016 | PRIMARY | RECORD | NULL | S || 3011491639016 | PRIMARY | RECORD | NULL | S || 3011491639016 | PRIMARY | RECORD | NULL | S |+-----------------------+------------+-----------+------------------------+-----------+


哈!你看,在LOCK_DATA列中有NULL。可是请不要担忧,这只是将信息呈现给人类的方式-Lock System仍然知道哪一个页面的heap_no被锁定,若是您尝试从另外一个客户端访问这些记录,则必须等待:

con2> DELETE FROM t WHERE id = 2;

若是在LOCK_DATA中看到NULL,请不要惊慌。这仅表示该页面当前在缓冲池中不可用。

正如你所指望的,运行DELETE会将页面带到内存,你如今能够看到数据没有问题:

> SELECT ENGINE_TRANSACTION_ID,INDEX_NAME,LOCK_DATA,LOCK_MODE,LOCK_STATUS  FROM performance_schema.data_locks  WHERE OBJECT_NAME='t' AND LOCK_TYPE='RECORD';+-----------------------+------------+------------------------+---------------+-------------+| ENGINE_TRANSACTION_ID | INDEX_NAME | LOCK_DATA | LOCK_MODE | LOCK_STATUS |+-----------------------+------------+------------------------+---------------+-------------+| 2775 | PRIMARY | 2 | X,REC_NOT_GAP | WAITING || 284486501679344 | PRIMARY | supremum pseudo-record | S | GRANTED || 284486501679344 | PRIMARY | 1 | S | GRANTED || 284486501679344 | PRIMARY | 2 | S | GRANTED || 284486501679344 | PRIMARY | 5 | S | GRANTED || 284486501679344 | PRIMARY | 4 | S | GRANTED |+-----------------------+------------+------------------------+---------------+-------------+


锁拆分

如前所述,“轴”与“点”和“点之间的间隙”(理论上)能够在锁系统中以两种不一样的方式建模:

  • 选项A:两种不一样的资源。间隙(15,33)是一种资源,而点[33]是另外一种资源。可使用一组简单的访问模式(例如,仅XS独立地请求和授予每种权限

  • 选项B:一个单一的资源,用于记录和前面的间隙的组合,以及一组更宽的访问模式,用于对间隙和记录作的事情进行编码(X,  X,REC_NOT_GAPX,GAPSS,REC_NOT_GAPS,GAP,...)

InnoDB(目前)使用选项B我看到的主要好处是在常见的状况下(当事务须要在扫描期间锁定间隙和记录时),它只须要一个内存中的对象便可,而不是两个,这不只节省了空间,并且须要更少的内存查找以及对列表中的单个对象使用快速路径。

可是,这种设计决策并不是一成不变,由于从概念上讲,它认为X = X,GAP + X,REC_NOT_GAPS = S,GAP + S,REC_NOT_GAP 而且InnoDB 8.0.18能够经过下面描述的所谓的“锁拆分”技术来利用这些方程式。

事务必须等待甚至死锁的常见缘由是由于它已经有记录但没有间隙(例如,它具备X,REC_NOT_GAP)而且必须“升级”以弥补在记录以前的间隙(例如,它请求X),惋惜它不得不等待另外一个事务(例如,另外一个事务正在等待S,REC_NOT_GAP)。(一般,事务不能忽略仍在等待的请求是为了不使等待者饿死。您能够在deadlock_on_lock_upgrade.test中看到这种状况的详细描述

“锁拆分”技术使用上面给出的方程式,并从它们得出needed - possessed = missing:在咱们的示例中:
X – X,REC_NOT_GAP = X,GAP
所以对X的事务请求被悄悄地转换为更适度的请求:仅针对X ,GAP在这种特殊状况下,这意味着能够当即授予该请求(回想一下*,GAP请求没必要等待任何东西),从而避免了等待和死锁。

二级索引

如前所述,每一个索引均可以看做是一个单独的轴,具备本身的点和间隙,能够锁定这些点和间隙,这会稍微有些复杂。经过遵循一些常识规则,您可能会发现本身对于给定的查询必须锁定哪些点和间隙。基本上,您要确保若是某个事务修改了会影响另外一事务的结果集的内容,则此读取事务所需的锁必须与进行修改的事务所需的锁互斥,而无论查询计划如何。有几种方法能够设计规则来实现这一目标。

例如,考虑一个简单的表:

CREATE TABLE point2D( x INT NOT NULL PRIMARY KEY, y INT NOT NULL UNIQUE );INSERT INTO point2D (x,y) VALUES (0,3),  (1,2),  (3,1), (2,0);

让咱们尝试经过如下方式找出须要哪些锁:

DELETE FROM point2D WHERE x=1;

有两个轴:x和y。彷佛合理的是咱们至少应锁定x轴上的point(1)。y轴呢?咱们能够避免在y轴上锁定任何东西吗?老实说,我相信这取决于数据库的实现,可是请考虑

SELECT COUNT(*) FROM point2D WHERE y=2 FOR SHARE;

若是锁仅存储在x轴上,则必须运行。SELECT将从y列上的索引来找到匹配的行开始,可是要知道它是否被锁定,就必须知道其x值。这是一个合理的要求。实际上,InnoDB确实在每一个二级索引条目中存储了主键的列(示例中的x),所以在索引中为y查找x的值并不重要。可是,请回想一下,在InnoDB中,锁并不真正与x的值绑定(例如,这多是一个至关长的字符串),而是与heap_no(咱们用做位图中的偏移量的短数字)相关联–您须要知道heap_no检查锁的存在。所以,您如今必须进入主索引并加载包含该记录的页,以便了解该记录的heap_no值

另外一种方法是确保不管使用哪一个索引来查找x = 1的行,它的锁将被发现,而不须要查阅任何其余索引。这能够经过将点锁定在y轴上且由y = 2来完成。上面提到SELECT查询在尝试获取本身的锁时将看到它已被锁定。SELECT应该什么锁一样,这能够经过几种方式实现:它能够仅锁定y = 2的y轴上的点,或者也能够跳至主索引并使用x = 1锁定x上的点。正如我已经说过的,出于性能缘由,第一种方法彷佛更快,由于它避免了在主索引中的查找。

让咱们看看咱们的怀疑是否符合现实。首先,让咱们检查经过二级索引进行选择的事务持有的锁(有时,优化器会选择一个扫描主索引的查询计划,而不是使用一个二级索引,即便在您认为这是疯狂的查询——在这样的决策中存在探索/利用权衡。此外,咱们人类关于什么更快的直觉多是错误的))

con1> BEGIN;con1> SELECT COUNT(*) FROM point2D WHERE y=2 FOR SHARE;con1> SELECT INDEX_NAME,LOCK_TYPE,LOCK_DATA,LOCK_MODE  FROM performance_schema.data_locks WHERE OBJECT_NAME='point2D';+------------+-----------+-----------+---------------+| INDEX_NAME | LOCK_TYPE | LOCK_DATA | LOCK_MODE |+------------+-----------+-----------+---------------+| NULL | TABLE | NULL | IS || y | RECORD | 2, 1 | S,REC_NOT_GAP |+------------+-----------+-----------+---------------+

这符合咱们的指望。咱们看到整个表(IS上有一个意图锁,而且特定记录上有一个锁,但以前没有间隙(S,REC_NOT_GAP),二者都是“共享的”。请注意,LOCK_DATA列将该记录描述为2,1,由于它以与存储在该行的辅助索引条目中的顺序相同的顺序列出各列。首先是索引列(y),而后是缺乏的主键片断( X)。因此2,1表示<y = 2,x = 1>。

让咱们用ROLLBACK使该事务返回到原始状态,咱们检查一下DELETE单独使用了哪些锁

con1> COMMIT;con1> BEGIN;con1> DELETE FROM point2D WHERE x=1;con1> SELECT INDEX_NAME,LOCK_TYPE,LOCK_DATA,LOCK_MODE FROM performance_schema.data_locks WHERE OBJECT_NAME='point2D';+------------+-----------+-----------+---------------+| INDEX_NAME | LOCK_TYPE | LOCK_DATA | LOCK_MODE |+------------+-----------+-----------+---------------+| NULL | TABLE | NULL | IX || PRIMARY | RECORD | 1 | X,REC_NOT_GAP |+------------+-----------+-----------+---------------+

哈,这是使人费解的:咱们在整个表(IX)上看到了预期的意图锁,咱们在主索引记录自己上看到了锁,二者都是“独占的”,但咱们在二级索引上没有看到任何锁。若是DELETE只在主索引上加锁,SELECT只在二级索引上加锁,那么InnoDB如何防止二者并发执行呢?让咱们保持这个删除事务打开,并启动另外一个客户端,看看它是否可以看到删除的行:

con2> BEGIN;con2> SELECT COUNT(*) FROM point2D WHERE y=2 FOR SHARE;


嗯..SELECT被阻止了(很好),让咱们检查Performance_schema.data_locks以肯定状况如何:

con1> SELECT ENGINE_TRANSACTION_ID trx_id,INDEX_NAME,LOCK_TYPE,LOCK_DATA,LOCK_MODE,LOCK_STATUS  FROM performance_schema.data_locks WHERE OBJECT_NAME='point2D';+-----------------+------------+-----------+-----------+---------------+-------------+| trx_id | INDEX_NAME | LOCK_TYPE | LOCK_DATA | LOCK_MODE | LOCK_STATUS |+-----------------+------------+-----------+-----------+---------------+-------------+| 283410363307272 | NULL | TABLE | NULL | IS | GRANTED || 283410363307272 | y | RECORD | 2, 1 | S | WAITING || 1560 | NULL | TABLE | NULL | IX | GRANTED || 1560 | PRIMARY | RECORD | 1 | X,REC_NOT_GAP | GRANTED || 1560 | y | RECORD | 2, 1 | X,REC_NOT_GAP | GRANTED |+-----------------+------------+-----------+-----------+---------------+-------------+

哈!咱们的事务(283410363307272)正在等待获取二级索引记录<y = 2,x = 1>上S锁(及其前面的间隙),咱们能够看到它必须等待的缘由多是该事务正在执行DELETE( 1560)使用X,REC_NOT_GAP锁定相同的<y = 2,x = 1> 

可是……当咱们检查1560持有的锁时,仅仅一秒钟以前咱们尚未看到任何这样的锁–这个锁只是如今才出现,怎么来的?鉴于1560目前尚未“主动作任何事情”,这更加使人困惑-它如何得到锁?

回想一下Performance_schema.metadata_locks仅显示显式锁,但不显示隐式锁,而且隐式锁能够在须要跟踪谁必须等待谁时当即转换为显式锁。实际上,这意味着当283410363307272请求锁系统授予对<y = 2,x = 1>的S锁时,锁系统首先检查这条记录上是否存在它能够推断的隐式锁。这是一个至关复杂的过程(您能够尝试从源代码lock_sec_rec_some_has_impl 开始跟踪

  • 检查page_get_max_trx_id(page)的值——对于每一个页面,咱们存储了修改过这个二级索引页的全部事务的最大id。删除操做确实将它“撞”到它本身的id(除非它已经更大了)

  • 而后,咱们将max_trx_id与一些trx_rw_min_trx_id()进行比较,将跟踪仍处于活动状态的事务中的最小ID。换句话说,咱们试探性地肯定某个活动事务是否有可能对二级索引具备隐式锁,并在此处进行一些权衡:

    • 二级索引,咱们不跟踪每一个记录的max_trx_id ,咱们跟踪它整个页面,所以会使用更少的存储,咱们可能会假意地认为,咱们的记录被修改是合理的,尽管实际上这种修改是应用到同一页上的其余记录

    • 咱们不会很是仔细地检查这个trx ID是否属于活动事务集,而只是将其与其中的最小ID进行比较(坦率地说,鉴于先前的简化,咱们必须采用这种方式来保持正确性:不知道修改该行事务的实际ID,仅知道其上限)

  • 若是进行试探后发现没有人对此记录持有隐式锁,咱们能够在这里中止,由于没有活动的事务的ID低于此页面上提到的修改记录的事务的最大ID。这意味着咱们没必要查询主索引。

  • 不然,事情会变得混乱。咱们进入row_vers_impl_x_locked,它将:

    • 在主索引中定位记录(在某些状况下,因为与清除线程的竞争,该记录可能已经丢失

    • 检索最后一个事务的trx_id来修改此特定行(请注意,这是上面第一个启发式方法的更精确的模拟),而且

    • 检查trx_id是否仍处于活动状态(请注意,这是如何更精确地模拟上面的第二个启发式)

    • 若是事务仍然处于活动状态,则可能仍然是*在二级索引*上没有隐式锁。您会看到,它能够修改一些非索引的列,在这种状况下,二级索引条目在概念上不受影响,所以不须要隐式锁。为了进行检查,咱们必须繁琐地检索该行的先前版本,并精确地检查是否有任何索引列受到某种方式的影响,这在概念上意味着须要锁定。这很是复杂。我不会在这里解释,可是若是您好奇,能够在row_clust_vers_matches_sec  row_vers_impl_x_locked_low中阅读个人注释

  • 最后,若是认为隐式锁是必需的,则表明其合法全部者(主索引记录头中的trx_id)将其转换为显式锁(始终为X,REC_NOT_GAP类型)。

这里的重点是,在最坏的状况下,您不只须要从undo日志中检索主索引记录,还须要检索其先前版本,目的是为了肯定是否存在隐式锁。在最佳状况下,您只需查看二级索引页面并说“ 没有”。

好的,因此看起来线程执行DELETE有些懒惰,而且SELECT线程正在作一些额外的工做来使DELETE式的内容变得明确

可是,这应该使您感到好奇。若是首先执行SELECT操做,而后再开始DELETE-若是SELECT 仅锁定二级索引,而且DELETE彷佛没有得到任何二级索引锁,那么怎么可能被未提交的SELECT阻止呢?在这种状况下,咱们也执行隐式到显式的转换吗?考虑到SELECT不该修改任何行,所以不该将其trx_id放在行或页面标题中,这彷佛是不可信的,所以没有任何痕迹能够推断出隐式锁。

也许咱们发现了一个错误?让咱们回滚

con1> ROLLBACK;con2> ROLLBACK;

并检查如下新场景:

con2> BEGIN;con2> SELECT COUNT(*) FROM point2D WHERE y=2 FOR SHARE;+----------+| COUNT(*) |+----------+| 1 |+----------+

如今在另外一个客户端DELETE

con1> BEGIN;con1> DELETE FROM point2D WHERE x=1;

彷佛没有错误,就像等待DELETE同样。让咱们看看显式锁:

> SELECT ENGINE_TRANSACTION_ID trx_id,INDEX_NAME,LOCK_TYPE,LOCK_DATA,LOCK_MODE,LOCK_STATUS  FROM performance_schema.data_locks WHERE OBJECT_NAME='point2D';+-----------------+------------+-----------+-----------+---------------+-------------+| trx_id | INDEX_NAME | LOCK_TYPE | LOCK_DATA | LOCK_MODE | LOCK_STATUS |+-----------------+------------+-----------+-----------+---------------+-------------+| 2077 | NULL | TABLE | NULL | IX | GRANTED || 2077 | PRIMARY | RECORD | 1 | X,REC_NOT_GAP | GRANTED || 2077 | y | RECORD | 2, 1 | X,REC_NOT_GAP | WAITING || 283410363307272 | NULL | TABLE | NULL | IS | GRANTED || 283410363307272 | y | RECORD | 2, 1 | S,REC_NOT_GAP | GRANTED |+-----------------+------------+-----------+-----------+---------------+-------------+


给超级敏锐读者的技术说明:283410363307272不只是一个可疑的长数字,并且与咱们在前面的示例中看到的ID彻底相同。这两个谜团的解释很简单:对于只读事务,InnoDB不会浪费分配真正单调事务ID的时间,而是从trx的内存地址临时派生它)

很酷,咱们获得的结果与前一个结果有些对称,可是此次是SELECT具备GRANTED锁,DELETE具备WAITING的(另外一个区别是,这一次SELECTS,REC_NOT_GAP而不是S,坦率地说,我不记得为何咱们还须要前一种状况的间隙锁)

好的,即便咱们看到DELETE单独执行并无建立这样的锁,为何如今正在执行的DELETE事务具备显式的WAITING锁?

答案是:DELETE确实尝试对二级索引进行了锁定(经过调用lock_sec_rec_modify_check_and_lock),但这涉及到棘手的优化:当Lock System肯定能够授予这个锁时(由于已经没有冲突锁,因此咱们不建立显式锁),克制了它,由于调用者通知它能够根据须要推断出隐式锁。(为何?可能避免分配lock_t对象:考虑一个DELETE  操做会影响在主键上造成连续范围的许多行–与它们对应的二级索引条目可能无处不在,所以没法从压缩机制中受益。另外,只要InnoDB中有使用隐式锁的地方,您都必须检查它们,而且若是不管如何都必须检查隐式锁,那么您可能会在适用的状况下使用它们,由于你已经付过“检查费”了)

在咱们的案例中,锁系统肯定存在冲突,所以建立了一个明确的等待锁来跟踪它。

总而言之,当前版本的InnoDB使用哪一种解决方案来防止DELETESELECT二级索引之间的冲突

  • DELETE锁定两个索引,SELECT锁定一个?

  • DELETE仅锁定主要对象,SELECT检查二者?

它很复杂,但更像第一种方法,但要注意的是,DELETE在任何可能的状况二级索引上的锁都是隐式的。

好的,如今咱们已经准备好讨论死锁检测,这是咱们的下一个话题。

感谢您使用MySQL!

感谢您关注“MySQL解决方案工程师”!






本文分享自微信公众号 - MySQL解决方案工程师(mysqlse)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索