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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索