當Mysql - InnoDB行鎖遇到複合主鍵和多列索引

背景

今天在配合其餘項目組作系統壓測,過程當中出現了偶發的死鎖問題。分析代碼後發現有複合主鍵的update狀況,更新複合主鍵表時只使用了一個字段更新,同時在事務內又有對該表的insert操做,結果出現了偶發的死鎖問題。算法

好比表t_lock_test中有兩個主鍵都爲primary key(a,b),可是更新時卻經過update t_lock_test .. where a = ?,而後該事務內又有insert into t_lock_test values(...)sql

InnoDB中的鎖算法是Next-Key Locking,極可能是由於這個點致使的死鎖,可是複合主鍵下會觸發Next-Key Locking嗎,那多列聯合unique索引下又會觸發Next-Key Locking嗎,書上並無找到答案,得實際測試一下。數據庫

InnoDB中的鎖

鎖是數據庫系統區別於文件系統的一個關鍵特性。鎖機制用於管理對共享資源的併發訪[插圖]。InnoDB存儲引擎會在行級別上對錶數據上鎖,這當然不錯。不過InnoDB存儲引擎也會在數據庫內部其餘多個地方使用鎖,從而容許對多種不一樣資源提供併發訪問。例如,操做緩衝池中的LRU列表,刪除、添加、移動LRU列表中的元素,爲了保證一致性,必須有鎖的介入。數據庫系統使用鎖是爲了支持對共享資源進行併發訪問,提供數據的完整性和一致性。

因爲使用鎖時基本都是在InnoDB存儲引擎下,因此跳過MyISAM,直接討論InnoDB。併發

鎖類型

InnoDB存儲引擎實現了以下兩種標準的行級鎖:測試

  • 共享鎖(S Lock),容許事務讀一行數據
  • 排它鎖(x lOCK),容許事務刪除或更新一條數據

若是一個事務T1已經得到了r的共享鎖,那麼另外的事務T2能夠當即得到行r的共享鎖,由於讀取並無改變r的數據,成這種狀況爲鎖兼容(Lock Compatible)。但如有其餘的事務T3箱得到行r的排它鎖,則其必須等待T一、T2釋放行r上的共享鎖 —— 這種狀況稱爲鎖不兼容。優化

排它鎖和共享鎖的兼容性:設計

\ X S
X 不兼容 不兼容
S 不兼容 兼容

InnoDB中對數據進行UPDATE/DELETE操做會產生行鎖,也能夠顯示的添加行鎖(也就是平時所說的「悲觀鎖」)code

select for update
lock in share mode

鎖算法

InnoDB有3種行鎖的算法,其分別是:索引

Record Lock:單個行記錄上的鎖,就是字面意思的行鎖

Record Lock會鎖住索引記錄(注意這裏說的是索引,由於InnoDB下主鍵索引即數據),若是 InnoDB存儲引擎表在創建的時候沒有設置任何一個索引,那麼這時對InnoDB存儲引擎會使用隱式的主鍵來進行鎖定。事務

Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄自己

Next-Key Lock:Gap Lock+Record Lock,鎖定一個範圍,而且鎖定記錄自己

Gap Lock和Next-Key Lock的鎖定區間劃分原則是同樣的。

例如一個索引有10/11/13和20這四個值,那麼該索引被劃分的的區間爲:

(-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)

採用Next-Key Lock的鎖定技術稱爲Next-Key Locking。其設計的目的是爲了解決Phantom Problem,這將在下一小節中介紹。而利用這種鎖定技術,鎖定的不是單個值,而是一個範圍,是謂詞鎖(predict lock)的一種改進。

當查詢的索引含有惟一(unique)屬性時(主鍵索引,惟一索引)InnoDB存儲引擎會對Next-Key Lock優化,將其降級爲Record Lock,即僅鎖住索引自己,不是範圍。

下面來看一個輔助索引(非惟一索引)下的鎖示例:

CREATE TABLE z ( a INT, b INT, PRIMARY KEY(a), KEY(b) );

INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;

表z的列b是輔助索引,若果事務A中執行:

SELECT * FROM z WHERE b=3 FOR UPDATE

因爲b列是輔助索引,因此此時會使用Next-Key Locking算法,鎖定的範圍是(1,3]。特別注意,InnoDB還會對輔助索引的下一個值加上Gap Lock,即還有一個輔助索引範圍爲(3,6]的鎖。所以,若在新事務B中運行如下SQL,都會被阻塞:

1. SELECT * FROM z WHERE a = 5 LOCK IN SHARE MODE;//S鎖
2. INSERT INTO z SELECT 4,2;
3. INSERT INTO z SELECT 6,5;

第1個SQL不能執行,由於在事務A中執行的SQL已經對彙集索引中列a=5的值加上X鎖,所以執行會被阻塞。

第2個SQL,主鍵插入4,沒有問題,可是插入的輔助索引值2在鎖定的範圍(1,3]中,所以執行一樣會被阻塞。

第3個SQL,插入的主鍵6沒有被鎖定,5也不在範圍(1,3]之間。但插入的b列值5在另下一個Gap Lock範圍(3,6]中,故一樣須要等待。

而下面的SQL語句,因爲不在Next-Key Lock和Gap Lock範圍內,不會被阻塞,能夠當即執行:

INSERT INTO z SELECT 8,6;
INSERT INTO z SELECT 2,0;
INSERT INTO z SELECT 6,7;

從上面的例子能夠發現,Gap Lock的做用是爲了組織多個事務將數據插入到統一範圍內,這樣會致使幻讀問題(Phantom Problem)。例子中事務A已經鎖定了b=3的記錄。若此時沒有Gap Lock鎖定(3,6],其餘事務就能夠插入索引b列爲3的記錄,這會致使事務A中的用戶再次執行一樣查詢會返回不一樣的記錄,即致使幻讀問題的產生。

用戶也能夠經過如下兩種方式來顯示的關閉Gap Lock(但不推薦):

  • 將事務的隔離級別設置爲READ COMMITED
  • 將參數innodb_locks_unsafe_for_binlog設置爲1

在InnoDB中,對於Insert的操做,會檢查插入記錄的下一條記錄是否被鎖定,若已經被鎖定,則不容許插入。對於上面的例子,事務A已經鎖定了表z中b=3的記錄,即已經鎖定了(1,3]的範圍,這時若在其餘事務中執行以下插入也會致使阻塞:

INSERT INTO z SELECT 2,2

由於在輔助索引列b上插入值爲2的記錄時,會監測到下一個記錄3已經被索引,修改b列值後,就能夠執行了

INSERT INTO z SELECT 2,0

幻讀(Phantom Problem)

幻讀是指在同一事務下,連續執行兩次一樣的SQL語句可能會致使不一樣的結果,第二次的SQL可能會返回以前不存在的行。

好比在同一個事務內,執行兩次查詢select x from t_lock_test where status = 'effective',或者第二次換一種方式查詢t_lock_teset表,若是有幻讀問題就會出現二次查詢結果不一致問題。

在默認的事務隔離級別(REPEATABLE READ)下,InnoDB存儲引擎採用Next—Key Locking機制來避免幻讀問題。

復(聯)合主鍵與鎖

上面的鎖機制介紹(摘自《Mysql技術內幕 InnoDB存儲引擎 第2版》),只是針對輔助索引和彙集索引,那麼複合主鍵下行鎖的表現形式又是怎麼樣呢?從書上並無找到答案,實際來測試一下。

首先建立一個複合主鍵的表

CREATE TABLE `composite_primary_lock_test` (
  `id1` int(255) NOT NULL,
  `id2` int(255) NOT NULL,
  PRIMARY KEY (`id1`,`id2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

INSERT INTO `composite_primary_lock_test`(`id1`, `id2`) VALUES (10, 10);
INSERT INTO `composite_primary_lock_test`(`id1`, `id2`) VALUES (1, 8);
INSERT INTO `composite_primary_lock_test`(`id1`, `id2`) VALUES (3, 6);
INSERT INTO `composite_primary_lock_test`(`id1`, `id2`) VALUES (5, 6);
INSERT INTO `composite_primary_lock_test`(`id1`, `id2`) VALUES (3, 3);
INSERT INTO `composite_primary_lock_test`(`id1`, `id2`) VALUES (1, 1);
INSERT INTO `composite_primary_lock_test`(`id1`, `id2`) VALUES (5, 1);
INSERT INTO `composite_primary_lock_test`(`id1`, `id2`) VALUES (7, 1);

事務A先來查詢id2=6的列,並添加行鎖

select * from composite_primary_lock_test where id2 = 6 lock in share mode

此時的鎖會降級到Record Lock嗎?事務B Update一條Next-Key Lock範圍內的數據(id1=1,id2=8)證實一下:

UPDATE `composite_primary_lock_test` SE WHERE `id1` = 1 AND `id2` = 8;

結果是UPDATE被阻塞了,那麼再來試試加鎖時在where中把兩個主鍵都帶上:

select * from composite_primary_lock_test where id2 = 6 and id1 = 5 lock in share mode

執行UPDATE

UPDATE `composite_primary_lock_test` SE WHERE `id1` = 1 AND `id2` = 8;

結果是UPDATE沒有被阻塞

上面加鎖的id2=6的數據,不僅1條,那麼再試試對惟一的數據id2=8,只根據一個主鍵加鎖呢,會不會降級爲行級鎖:

select * from composite_primary_lock_test where id2 = 8 lock in share mode;
UPDATE `composite_primary_lock_test` SE WHERE `id1` = 12 AND `id2` = 10;

結果也是被阻塞了,實驗證實:

複合主鍵下,若是加鎖時不帶上全部主鍵,InnoDB會使用Next-Key Locking算法,若是帶上全部主鍵,纔會看成惟一索引處理,降級爲Record Lock,只鎖當前記錄。

多列索引(聯合索引)與鎖

上面只驗證了複合主鍵下的鎖機制,那麼多列索引呢,會不會和複合索引機制相同?多列unique索引呢?

新建一個測試表,並初始化數據

CREATE TABLE `multiple_idx_lock_test` (
  `id` int(255) NOT NULL,
  `idx1` int(255) NOT NULL,
  `idx2` int(255) DEFAULT NULL,
  PRIMARY KEY (`id`,`idx1`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

ALTER TABLE `multiple_idx_lock_test` 
ADD UNIQUE INDEX `idx_multi`(`idx1`, `idx2`) USING BTREE;

INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (1, 1, 1);
INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (5, 2, 2);
INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (7, 3, 3);
INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (4, 4, 4);
INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (2, 4, 5);
INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (3, 5, 5);
INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (8, 6, 5);
INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (6, 6, 6);

事務A查詢增長S鎖,查詢時僅使用idx1列,並遵循最左原則:

select * from multiple_idx_lock_test where idx1 = 6 lock in share mode;

如今插入一條Next-Key Lock範圍內的數據:

INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (9, 6, 7);

結果是被阻塞了,再試一遍經過多列索引中全部字段來加鎖:

select * from multiple_idx_lock_test where idx1 = 6 and idx2 = 6 lock in share mode;

插入一條Next-Key Lock範圍內的數據:

INSERT INTO `multiple_idx_lock_test`(`id`, `idx1`, `idx2`) VALUES (9, 6, 7);

結果是沒有被阻塞

因而可知,當使用多列惟一索引時,加鎖須要明確要鎖定的行(即加鎖時使用索引的全部列),InnoDB纔會認爲該條記錄爲惟一值,鎖纔會降級爲Record Lock。不然會使用Next-Key Lock算法,鎖住範圍內的數據。

總結

在使用Mysql中的鎖時要謹慎使用,尤爲時更新/刪除數據時,儘可能使用主鍵更新,若是在複合主鍵表下更新時,必定經過全部主鍵去更新,避免鎖範圍變大帶來的死鎖等問題。

參考

  • 《Mysql技術內幕 InnoDB存儲引擎 第2版》 - 姜承堯
相關文章
相關標籤/搜索