1. 程式人生 > 其它 >無法使用閂鎖型別 sh 讀取並閂鎖頁_InnoDB資料鎖第2.5部分“鎖”(深入研究)...

無法使用閂鎖型別 sh 讀取並閂鎖頁_InnoDB資料鎖第2.5部分“鎖”(深入研究)...

技術標籤:無法使用閂鎖型別 sh 讀取並閂鎖頁

作者:Kuba Łopuszański 譯:徐軼韜

現在,我們將InnoDB資料鎖-第2部分“鎖”中瞭解到的所有知識放在一起,進行深入研究:

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)

我們看到:

  • 第一個SELECT * FROM t FOR SHARE;在5、10、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”!)開始,檢視頁面是否是放置記錄的正確位置,然後在插入點右側閂住鎖系統佇列並檢查是否有*,GAP

    SX鎖。我們的例子中沒有記錄,因此我們立即著手插入記錄(它有一個隱式鎖,因為它在“last modified by”欄位中有我們的事務的id,希望這解釋了為什麼在記錄4上沒有顯式的X,REC_NOT_GAP鎖)。相反的情況是存在一些衝突的鎖,為了顯式地跟蹤衝突,將建立一個等待的INSERT_INTENTION鎖,以便在授予操作後可以重試。最後一步是在軸上插入新點會將已經存在的間隙分成兩部分。對於舊間隙,已經存在的任何鎖都必須繼承到插入點左側新建立的間隙。這就是我們在第4行看到S,GAP的原因:它是從第5行的S鎖繼承的。

這只是涉及到的真正複雜問題的冰山一角(我們還沒有討論從已刪除的行繼承鎖,二級索引,唯一性檢查..),但是從中可以得到一些更深層次的想法:

  • 通常,要提供可序列性,您需要“鎖定所見內容”,這不僅包括點,而且還包括點之間的間隙。如果您可以想象查詢在掃描時如何訪問表,那麼您大都可以猜測它將必須鎖定什麼。這意味著擁有良好的索引很重要,這樣您就可以直接跳到要鎖定的點,而不必鎖定整個掃描範圍。

  • 反之亦然:如果您不關心可序列性,您可以嘗試不鎖定某些東西。例如,在READ COMMITTED隔離級別較低的情況下,我們嘗試避免鎖定行之間的間隙(因此,其他事務可以在行之間插入行,這會導致所謂的“幻讀”)

  • 在InnoDB中,所有那些“正在插入”和“正在刪除”的行,實際上都存在於索引中,因此出現在軸上並將其分成多個間隙。這與某些其他引擎形成對比,其他引擎將正在進行的更改保留在“暫存區”中,並且僅在提交時將其合併。這意味著即使在概念上併發事務之間沒有互動(例如,在提交事務之前,我們不應該看到行被事務插入),但在低級別實現中,它們之間的互動仍然很多(例如,事務可以在尚未正式存在的行上有一個等待鎖)。因此,看到Performance_schema.data_locks報告尚未插入或已被刪除的行,不需要感到驚訝(後者將最終被清除)

記錄鎖的壓縮(以及丟失的LOCK_DATA)

在上面的示例中,您看到了一個非常有用的LOCK_DATA列,該列為您顯示了放置記錄鎖的索引列的行值。這對於分析情況非常有用,但是將“ LOCK_DATA”顯式儲存在記憶體物件中會很浪費,所以當你查詢performance_schema時,這些資料實際上是實時重建的。data_locks表來自鎖系統記憶體中可用的壓縮資訊,它與緩衝池頁面中的可用資料結合在一起。也就是說,鎖系統根據記錄所在的頁面和頁面中的記錄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:在我們的示例中:XX,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表示。

讓我們用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)正在等待獲取二級索引記錄上的S鎖(及其前面的間隙),我們可以看到它必須等待的原因可能是該事務正在執行DELETE( 1560)使用X,REC_NOT_GAP鎖定相同的。

但是……當我們檢查1560持有的鎖時,僅僅一秒鐘之前我們還沒有看到任何這樣的鎖–這個鎖只是現在才出現,怎麼來的?鑑於1560目前還沒有“主動做任何事情”,這更加令人困惑-它如何獲得鎖?

回想一下Performance_schema.metadata_locks僅顯示顯式鎖,但不顯示隱式鎖,並且隱式鎖可以在需要跟蹤誰必須等待誰時立即轉換為顯式鎖。實際上,這意味著當283410363307272請求鎖系統授予對的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解決方案工程師”!

87fbc7e59425a46a25f9e6ae7df6ecdb.png