共計 8109 個字符,預計需要花費 21 分鐘才能閱讀完成。
自動寫代碼機器人,免費開通
這篇文章主要介紹 MySQL 中鎖解決幻讀問題的方法,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
什么是鎖
鎖是一種用于保證在并發場景下每個事務仍能以一致性的方式讀取和修改數據的方式,當一個事務對某一條數據上鎖之后,其他事務就不能修改或者只能阻塞等待鎖的釋放,所以鎖的粒度大小一定程度上可以影響到訪問數據庫的性能。
從鎖的粒度上來說,我們可以將鎖分為表鎖和行鎖。
表鎖
顧名思議,表鎖就是直接鎖表,在 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 鎖。
這樣的話上面的問題就迎刃而解了,當需要給一張表上表鎖的時候,只需要看這張表是否有對應的意向鎖就可以了,無需遍歷整張表。
各種鎖的兼容關系
下面這張圖是各種鎖的兼容關系,參考自官網:
XIXSISX
互斥
互斥
互斥
互斥
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 事務 BBEGIN;
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 事務 BBEGIN;
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 事務 BBEGIN;
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 事務 BBEGIN;
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 事務 BBEGIN;
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 事務 BBEGIN;
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 事務 BBEGIN;
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 引擎中的事務的唯一 IDtrx_state 事務狀態:RUNNING, LOCK WAIT, ROLLING BACK,COMMITTINGtrx_started 事務的開始時間 trx_requested_lock_id 等待會務的鎖 ID, 如果 trx_state 不為 LOCK WAIT 時,為 nulltrx_wait_started 事務等待開始的時間 trx_weight 事務的權重,反映了一個事務修改和鎖住的行數,當發生死鎖時候,InnoDB 會選擇該值最小的事務進行回滾 trx_mysql_thread_idMySQL 中的線程 ID,可以通過 SHOW PROCESSLIST 查詢 trx_query 事務運行的 sql 語句 trx_operation_state 事務的當前操作狀態,如果沒有則為 NULLtrx_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 上一張表的事務 IDlock_mode 鎖的模式: S, X, IS, IX, GAP, AUTO_INC,UNKNOWNlock_type 鎖的類型是表鎖還是行鎖 lock_table 被鎖住的表 lock_index 被鎖住的索引,表鎖則為 NULLlock_space 鎖記錄的空間 id,表鎖則為 NULLlock_page 事務鎖定頁的數量,表鎖則為 NULLlock_rec 事務鎖定行的數量,表鎖則為 NULLlock_data 事務鎖定的主鍵值,表鎖則為 NULLINNODB_LOCK_WAITS
記錄了鎖等待的信息。每個被阻塞的 InnoDB 事務包含一個或多個行,表示它所請求的鎖以及正在阻塞該請求的任何鎖。
列名含義 lock_id 鎖的 id(雖然 LOCK_ID 當前包含 TRX_ID,但 LOCK_ID 中的數據格式隨時可能更改,不要編寫解析 LOCK_ID 值的應用程序)requesting_trx_id 申請鎖資源的事務 IDrequested_lock_id 申請的鎖的 IDblocking_trx_id 阻塞的事務 IDblocking_lock_id 阻塞的鎖的 ID
以上是 MySQL 中鎖解決幻讀問題的方法的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注丸趣 TV 行業資訊頻道!
向 AI 問一下細節丸趣 TV 網 – 提供最優質的資源集合!