認識什麼是鎖,MySQL中鎖如何解決幻讀問題

2020-10-24 06:01:20

欄目介紹鎖如何解決幻讀問題。

前言

今天就為大家介紹一下MySQL中鎖相關的知識。

本文在沒有特別宣告的情況下,均是預設InnoDB引擎,如涉及到其他引擎或者資料庫則會特別指出。

什麼是鎖

鎖是一種用於保證在並行場景下每個事務仍能以一致性的方式讀取和修改資料的方式,當一個事務對某一條資料上鎖之後,其他事務就不能修改或者只能阻塞等待鎖的釋放,所以鎖的粒度大小一定程度上可以影響到存取資料庫的效能。

從鎖的粒度上來說,我們可以將鎖分為表鎖和行鎖。

表鎖

顧名思議,表鎖就是直接鎖表,在MyISAM引擎中就只有表鎖。

表鎖的加鎖方式為:

LOCK TABLE 表名 READ;--鎖定後表唯讀
UNLOCK TABLE; --解鎖複製程式碼

行鎖

行鎖,從名字上來看,就是鎖住一行資料,然而,行鎖的實際實現演演算法會相對複雜,有時候並不僅僅只是鎖住某一條資料,這個後面再展開。

正常的思路是:鎖住一行資料之後,其他事務就不能來存取這條資料了,那麼我們想象,假如事務A存取了一條資料,只是拿出來讀一下,並不想去修改,正好事務B也來存取這條資料,也僅僅只是想拿出來讀一下,並不想去修改,這時候如果因此阻塞了,就有點浪費效能了。所以為了優化這種讀資料的場景,我們又把行鎖分為了兩大型別:共用鎖和排他鎖

共用鎖

共用鎖,Shared Lock,又稱之為讀鎖,S鎖,就是說一條資料被加了S鎖之後,其他事務也能來讀資料,可以共用一把鎖。
我們可以通過如下語句加共用鎖:

select * from test where id=1 LOCK IN SHARE MODE;複製程式碼

加鎖之後,直到加鎖的事務結束(提交或者回滾)就會釋放鎖。

排他鎖

排他鎖,Exclusive Lock,又稱之為寫鎖,X鎖。就是說一條資料被加了X鎖之後,其他事務想來存取這條資料只能阻塞等待鎖的釋放,具有排他性。

當我們在修改資料,如:insert,update,delete的時候MySQL就會自動加上排他鎖,同樣的,我們可以通過如下sql語句手動加上排他鎖:

select * from test where id=1 for update;複製程式碼

在InnoDB引擎中,是允許行鎖和表鎖共存的。

但是這樣就會有一個問題,假如事務A給t表其中一行資料上鎖了,這時候事務B想給t表上一個表鎖,這時候怎麼辦呢?事務B怎麼知道t表有沒有行鎖的存在,如果採用全表遍歷的情況,當表中的資料很大的話,加鎖都要加半天,所以MySQL中就又引入了意向鎖

意向鎖

意向鎖為表鎖,分為兩種型別,分為:意向共用鎖(Intention Shared Lock)和意向排他鎖(Intention Exclusive Lock),這兩種鎖又分別可以簡稱為IS鎖和IX鎖。

意向鎖是MySQL自己維護的,使用者無法手動加意向。

意向鎖有兩大加鎖規則:

  • 當需要給一行資料加上S鎖的時候,MySQL會先給這張表加上IS鎖。
  • 當需要給一行資料加上X鎖的時候,MySQL會先給這張表加上IX鎖。

這樣的話上面的問題就迎刃而解了,當需要給一張表上表鎖的時候,只需要看這張表是否有對應的意向鎖就可以了,無需遍歷整張表。

各種鎖的相容關係

下面這張圖是各種鎖的相容關係,參考自官網:


XIXSIS
X

互斥

互斥

互斥

互斥

IX

互斥

共用

衝突

共用

S

互斥

互斥

共用

共用

IS

互斥

共用

共用

共用

鎖到底鎖的是什麼

建立以下兩張表,並初始化5條資料,注意test表有2個索引而test2沒有索引:

CREATE TABLE `test` (
  `id` int(11) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `NAME_INDEX` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test VALUE(1,'張1');
INSERT INTO test VALUE(5,'張5');
INSERT INTO test VALUE(8,'張8');
INSERT INTO test VALUE(10,'張10');
INSERT INTO test VALUE(20,'張20');

CREATE TABLE `test2` (
  `id` varchar(32) NOT NULL,
  `name` varchar(32) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test2 VALUE(1,'張1');
INSERT INTO test2 VALUE(5,'張5');
INSERT INTO test2 VALUE(8,'張8');
INSERT INTO test2 VALUE(10,'張10');
INSERT INTO test2 VALUE(20,'張20');複製程式碼

舉例猜測

在行鎖中,假如我們對一行記錄加鎖,那麼到底是把什麼東西鎖住了,我們來看下面兩個例子:
舉例1(操作test表):

事務A事務B
BEGIN;
SELECT * FROM test WHERE id=1 FOR UPDATE;

SELECT * FROM test WHERE id=1 FOR UPDATE;

阻塞


SELECT * FROM test WHERE id=5 FOR UPDATE;

加鎖成功

COMMIT;

(釋放鎖)



SELECT * FROM test WHERE id=1 FOR UPDATE;

加鎖成功

舉例2(操作test2表):

事務A事務B
BEGIN;
SELECT * FROM test2 WHERE id=1 FOR UPDATE;

SELECT * FROM test2 WHERE id=1 FOR UPDATE;

阻塞


SELECT * FROM test2 WHERE id=5 FOR UPDATE;

阻塞

COMMIT;

(釋放鎖)



SELECT * FROM test2 WHERE id=1 FOR UPDATE;

加鎖成功

從上面兩個例子我們可以發現,test表好像確實是鎖住了id=1這一行的記錄,而test2表好像不僅僅是鎖住了id=1這一行記錄,實際上經過嘗試我們就知道,test2表是被鎖表了,所以其實MySQL中InnoDB鎖住的是索引,當沒有索引的時候就會鎖表

接下來再看一個場景:

事務A事務B
BEGIN;
SELECT * FROM test WHERE name=‘張1’ FOR UPDATE;

SELECT name FROM test WHERE name=‘張1’ FOR UPDATE;

阻塞


SELECT id FROM test WHERE id=1 FOR UPDATE;

阻塞

COMMIT;

(釋放鎖)



SELECT id FROM test WHERE id=1 FOR UPDATE;

加鎖成功

這個例子中我們是把name索引鎖住了,然後我們在事務B中通過主鍵索引只查id,這樣就用到name索引了,但是最後發現也被阻塞了。所以我們又可以得出下面的結論,MySQL索引不但鎖住了輔助索引,還會把輔助索引對應的主鍵索引一起鎖住

到這裡,可能有人會有懷疑,那就是我把輔助索引鎖住了,但是假如加鎖的時候,只用到了覆蓋索引,然後我再去查主鍵會怎麼樣呢?

接下來讓我們再驗證一下:

事務A事務B
BEGIN;
SELECT name FROM test WHERE name=‘張1’ FOR UPDATE;

SELECT name FROM test WHERE name=‘張1’ FOR UPDATE;

阻塞


SELECT * FROM test WHERE id=1 FOR UPDATE;

阻塞


SELECT id FROM test WHERE id=1 FOR UPDATE;

阻塞

COMMIT;

(釋放鎖)



SELECT id FROM test WHERE id=1 FOR UPDATE;

加鎖成功

我們可以看到,就算只是用到了輔助索引加鎖,MySQL還是會把主鍵索引鎖住,而主鍵索引的B+樹葉子節點中,又儲存了整條資料,所以查詢任何欄位都會被鎖定。

到這裡,我們可以明確的給鎖到底鎖住了什麼下結論了:

結論

InnoDB引擎中,鎖鎖的是索引:

  • 假如一張表沒有索引,MySQL會進行鎖表(其實鎖住的是隱藏列ROWID的主鍵索引)
  • 假如我們對輔助索引加鎖,那麼輔助索引所對應的主鍵索引也會被鎖住
  • 主鍵索引被鎖住,實際上就等於是整條記錄都被鎖住了(主鍵索引葉子節點儲存了整條資料)

行鎖的演演算法

上一篇介紹事務的時候我們提到了,MySQL通過加鎖來防止了幻讀,但是如果行鎖只是鎖住一行記錄,好像並不能防止幻讀,所以行鎖鎖住一條記錄的話只是其中一種情況,實際上行鎖有三種演演算法:記錄鎖(Record Lock),間隙鎖(Gap Lock)和臨鍵鎖(Next-Key Lock),而之所以能做到防止幻讀,正是臨鍵鎖起的作用。

記錄鎖(Record Lock)

記錄鎖就是上面介紹的,當我們的查詢能命中一條記錄的時候,InnoDB就會使用記錄鎖,鎖住所命中的這一行記錄。

間隙鎖(Gap Lock)

當我們的查詢沒有命中記錄的時候,這時候InnoDB就會加上一個間隙鎖。

事務A事務B
BEGIN;
SELECT * FROM test WHERE id=1 FOR UPDATE;

INSERT INTO test VALUE (2,‘張2’);

阻塞


INSERT INTO test VALUE (3,‘張3’);

阻塞


SELECT * FROM test WHERE id=2 FOR UPDATE;

加鎖成功

COMMIT;

(釋放鎖)


從上面的例子中,我們可以得出結論:

  • 間隙鎖與間隙鎖之間不衝突,也就是事務A加了間隙鎖,事務B可以在同一個間隙中加間隙鎖。(之所以會用到間隙鎖就是沒有命中資料的時候,所以並沒有必要去阻塞讀,也沒有必要阻塞其他事務對同一個間隙加鎖)
  • 間隙鎖主要是會阻塞插入操作

間隙是如何確定的

test表中有5條記錄,主鍵值分別為:1,5,8,10,20。那麼就會有如下六個間隙:
(-∞,1),(1,5),(5,8),(8,10),(10,20),(20,+∞)

而假如主鍵不是int型別,那麼就會轉化為ASCII碼之後再確定間隙。

臨鍵鎖(Next-Key Lock)

臨鍵鎖就是記錄鎖和間隙鎖的結合。當我們進行一個範圍查詢,不但命中了一條或者多條記錄,且同時包括了間隙,這時候就會使用臨鍵鎖,臨鍵鎖是InnoDB中行鎖的預設演演算法。

注意了,這裡僅針對RR隔離級別,對於RC隔離級除了外來鍵約束和唯一性約束會加間隙鎖,沒有間隙鎖,自然也就沒有了臨鍵鎖,所以RC級別下加的行鎖都是記錄鎖,沒有命中記錄則不加鎖,所以RC級別是沒有解決幻讀問題的

臨鍵鎖在以下兩個條件時會降級成為間隙鎖或者記錄鎖:

  • 當查詢未命中任務記錄時,會降級為間隙鎖。
  • 當使用主鍵或者唯一索引命中了一條記錄時,會降級為記錄鎖。
事務A事務B
BEGIN;
SELECT * FROM test WHERE id>=2 AND id<=6 FOR UPDATE;



INSERT INTO test VALUE (2,‘張2’);

阻塞


INSERT INTO test VALUE (6,‘張6’);

阻塞


INSERT INTO test VALUE (8,‘張8’);

阻塞


SELECT * FROM test WHERE id=8 FOR UPDATE;

阻塞


INSERT INTO test VALUE (9,‘張9’);

插入成功

COMMIT;

(釋放鎖)


上面這個例子,事務A加的鎖跨越了(1,5)和(5,8)兩個間隙,且同時命中了5,然後我們發現我們對id=8這條資料進行操作也阻塞了,但是9這條記錄插入成功了。

臨鍵鎖加鎖規則

臨鍵鎖的劃分是按照左開右閉的區間來劃分的,也就是我們可以把test表中的記錄劃分出如下區間:(-∞,1],(1,5],(5,8],(8,10],(10,20],(20,+∞)。

那麼臨鍵鎖到底鎖住了哪些範圍呢?

**臨鍵鎖中鎖住的是最後一個命中記錄的 key 和其下一個左開右閉的區間**

那麼上面的例子中其實鎖住了(1,5]和(5,8]這兩個區間。

臨鍵鎖為何能解決幻讀問題

臨鍵鎖為什麼要鎖住命中記錄的下一個左開右閉的區間?答案就是為了解決幻讀。

我們想一想上面的查詢範圍id>=2且id<=6,如果我們事務A只鎖住了(1,5]這個區間,假如這時候事務B插入一條資料id=6,那麼事務A再去查詢,就會多出來了一條記錄id=6,就會出現了幻讀,所以我把你下一個區間5,10]也給鎖住,就可以避免了幻讀。

當然,其實如果我們執行的查詢剛好是id>=2且id<=5,那麼就算只鎖住了(1,5],同樣能避免幻讀問題,只是我們要考慮到查詢範圍的最大值沒有命中記錄的情況,而鎖住了下一個區間,可以確保不論是哪種範圍查詢,都可以避免幻讀的產生。

在我們使用鎖的時候,有一個問題是需要注意和避免的,我們知道,排它鎖有互斥的特性。一個事務持有鎖的時候,會阻止其他的事務獲取鎖,這個時候會造成阻塞等待,那麼假如事務一直等待下去,就會一直佔用CPU資源,所以,鎖等待會有一個超時時間,在InnoDB引擎中,可以通過引數:innodb_lock_wait_timeout查詢:

SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';複製程式碼

預設超時時間是50s,超時後會自動釋放鎖回滾事務。但是我們想一下,假如事務A在等待事務B釋放鎖,而事務B又在等待事務A釋放鎖,這時候就會產生一個等待環路了,而這種情況是無論等待多久都不可能會獲取鎖成功的,所以是沒有必要去等50s的,這種形成等待環路的現象又叫做死鎖。

死鎖(Dead Lock)

什麼是死鎖

死鎖是指的兩個或者兩個以上的事務在執行過程中,因為爭奪鎖資源而造成的一種互相等待的現象。

事務A事務B
BEGIN;
SELECT * FROM test WHERE id=10 FOR UPDATE;

BEGIN;

SELECT * FROM test WHERE id=20 FOR UPDATE;
SELECT * FROM test WHERE id=20 FOR UPDATE;

SELECT * FROM test WHERE id=10 FOR UPDATE;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
查詢出結果

在這裡插入圖片描述
我們可以看到,發生死鎖之後就會立刻回滾,而不會漫無目的的去等待50s之後超時再回滾事務,那麼MySQL是如何知道產生了死鎖的,是如何檢測死鎖的發生呢?

死鎖的檢測

目前資料庫大部分採用wait-for graph(等待圖)的方式來進行死鎖檢測,InnoDB引擎也是採用這種方式來檢測死鎖。資料庫中會記錄兩種資訊:

  • 鎖的資訊連結串列
  • 事務的等待連結串列
    wait-for graph 演演算法會根據這兩個資訊構建一張圖,當圖中存在迴路,則證明存在死鎖:
    如下圖中,t1和t2之間存在迴路,這就證明t1和t2事務之間存在死鎖
    在這裡插入圖片描述

死鎖的避免

  • 儘量將長事務拆分成多個小事務
  • 查詢時避免沒有where條件語句查詢,並儘可能使用索引查詢
  • 可以的話儘量使用等值查詢

鎖資訊查詢

InnoDB在information_schema庫下提供了3張表供我們查詢並排查事務和鎖相關問題。

INNODB_TRX

記錄了當前在InnoDB中執行的每個事務的資訊,包括事務是否在等待鎖、事務何時啟動以及事務正在執行的SQL語句(如果有的話)。

列名含義
trx_idInnoDD引擎中的事務的唯一ID
trx_state事務狀態:RUNNING, LOCK WAIT, ROLLING BACK,COMMITTING
trx_started事務的開始時間
trx_requested_lock_id等待會務的鎖ID,如果trx_state不為LOCK WAIT時,為null
trx_wait_started事務等待開始的時間
trx_weight事務的權重,反映了一個事務修改和鎖住的行數,當發生死鎖時候,InnoDB會選擇該值最小的事務進行回滾
trx_mysql_thread_idMySQL中的執行緒ID,可以通過SHOW PROCESSLIST查詢
trx_query事務執行的sql語句
trx_operation_state事務的當前操作狀態,如果沒有則為NULL
trx_tables_in_use當前事務中執行的sql語句用到的表數量
trx_tables_locked已經被鎖定表的數量(因為用的是行鎖,所以雖然顯示一張表被鎖了,但是可能只是鎖定的其中一行或幾行,所以其他行還是可以被其他事務存取)
trx_lock_structs當前事務保留的鎖數量
trx_lock_memory_bytes當前事務的索結構在記憶體中的大小
trx_rows_locked當前事務中鎖住的大致行數,包括已經被打上刪除標記等物理存在的但是對當前事務不可見的資料
trx_rows_modified當前事務修改或者插入的行數
trx_concurrency_tickets並行數,指的是當前事務未結束前仍然可以執行的並行數,可以通過系統變數innodb_concurrency_tickets設定
trx_isolation_level當前事務隔離級別
trx_unique_checks是否為當前事務開啟或者關閉唯一約束:0-否1-是
trx_foreign_key_checks是否為當前事務開啟或者關閉外來鍵約束:0-否1-是
trx_last_foreign_key_error最後一個外來鍵錯誤資訊,沒有則為空
trx_adaptive_hash_latched自適應雜湊索引是否被當前事務鎖定。在分割區自適應雜湊索引搜尋系統時,單個事務不會鎖定整個自適應雜湊索引。自適應雜湊索引分割區由innodb_adaptive_hash_index_parts控制,預設設定為8。
trx_adaptive_hash_timeout是立即放棄自適應雜湊索引的搜尋latch,還是在來自MySQL的呼叫中保留它。當沒有自適應雜湊索引爭用時,這個值將保持為零,並且語句會保留latch直到它們完成。在爭用期間,它的計數減少到零,並且語句在每一行查詢之後立即釋放鎖存。當自適應雜湊索引搜尋系統被分割區時(由innodb_adaptive_hash_index_parts控制),該值保持為0。
trx_is_read_only當前事務是否唯讀:0-否1-是
trx_autocommit_non_locking值為1表示這是一條不包含for update和lock in share model的語句,而且是在開啟autocommit情況下執行的有且僅有這一條語句,當這列和TRX_IS_READ_ONLY都為1時,InnoDB會優化事務以減少與更改表資料事務的相關開銷。

INNODB_LOCKS

記錄了事務請求鎖但未獲得的每個鎖的資訊和一個事務持有鎖但正在阻塞另一個事務的每個鎖的資訊。

列名含義
lock_id鎖的id(雖然LOCK_ID當前包含TRX_ID,但LOCK_ID中的資料格式隨時可能更改,不要編寫解析LOCK_ID值的應用程式)
lock_trx_id上一張表的事務ID
lock_mode鎖的模式: S, X, IS, IX, GAP, AUTO_INC,UNKNOWN
lock_type鎖的型別是表鎖還是行鎖
lock_table被鎖住的表
lock_index被鎖住的索引,表鎖則為NULL
lock_space鎖記錄的空間id,表鎖則為NULL
lock_page事務鎖定頁的數量,表鎖則為NULL
lock_rec事務鎖定行的數量,表鎖則為NULL
lock_data事務鎖定的主鍵值,表鎖則為NULL

INNODB_LOCK_WAITS

記錄了鎖等待的資訊。每個被阻塞的InnoDB事務包含一個或多個行,表示它所請求的鎖以及正在阻塞該請求的任何鎖。

列名含義
lock_id鎖的id(雖然LOCK_ID當前包含TRX_ID,但LOCK_ID中的資料格式隨時可能更改,不要編寫解析LOCK_ID值的應用程式)
requesting_trx_id申請鎖資源的事務ID
requested_lock_id申請的鎖的ID
blocking_trx_id阻塞的事務ID
blocking_lock_id阻塞的鎖的ID

更多相關免費學習推薦:(視訊)

以上就是認識什麼是鎖,MySQL中鎖如何解決幻讀問題的詳細內容,更多請關注TW511.COM其它相關文章!