InnoDB鎖
-
InnoDB實現了兩種標準的行級鎖
- 共享鎖(S Lock),允許事務讀一行數據
- 排他鎖(X Lock),允許事務刪除或更新一行數據
-
如果一個事務T1已經獲取了行R的共享鎖,那麼另外一個事務T2可以立即獲得行R的共享鎖,因爲讀取並沒有改變行R的數據,稱這種情況爲鎖兼容,如果有其他的事務T3想獲得行R的排他鎖,則必須等待事務T1、T2釋放行R上的共享鎖(稱鎖不兼容)。從下圖可以看出只有S與S鎖兼容,X和S都是行鎖,兼容是指對同一個記錄鎖的兼容情況。
X S X 不兼容 不兼容 X 不兼容 兼容 -
意向鎖:意向鎖是一種
不與行級鎖衝突表級鎖
-
意向鎖共享鎖(IS Lock),事務有意向對錶中的某些行加共享鎖(S鎖)
-- 事務要獲取某些行的 S 鎖,必須先獲得表的 IS 鎖。 SELECT column FROM table ... LOCK IN SHARE MODE;
-
意向排他鎖(IX Lock),事務有意向對錶中的某些行加排他鎖。
-- 事務要獲取某些行的 X 鎖,必須先獲得表的 IX 鎖。 SELECT column FROM table ... FOR UPDATE;
-
行鎖算法
-
MySQL的行鎖是在引擎層由各個引擎自己實現的。但並不是所有的引擎都支持行鎖,比如MyISAM引擎就不支持行鎖。InnoDB有3種行鎖的算法,分別是
- Record Lock:單個行記錄上的鎖
- Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄(是索引)本身
- Next-Key Lock: Gap Lock+Record Lock,鎖定一個範圍,並且鎖定記錄(是索引)本身
-
Record Lock總是會鎖定索引記錄,如果在建表時沒有建立任何索引,此時InnoDB引擎會使用隱式的主鍵來進行鎖定
-
Next-Key Lock是結合了 Gap Lock和Record Lock的一種鎖定算法(行鎖和間隙鎖的組合),在Next-Key Lock算法下,InnoDB對於行的查詢都是採用這種鎖定算法。利用這種鎖定技術鎖定的不是單個值,而是一個範圍(前開後閉區間)。當InnoDB掃描索引記錄的時候,會首先對索引記錄加上行鎖(Record Lock),再對索引記錄兩邊的間隙加上間隙鎖(Gap Lock)
-
**在InnoDB事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放,這個就是兩階段鎖協議。**如果你的事務中需要鎖多個行,要把最可能造成鎖衝突、最可能影響併發度的鎖儘量往後放。
-
當併發系統中不同線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會導致這幾個線程都進入無限等待的狀態,稱爲死鎖
-
如果你要刪除一個表裏面的前10000行數據
- 執行delete from T limit 10000;
- 在一個連接中循環執行20次 delete from T limit 500
- 在20個連接中同時執行delete from T limit 500
- 第一種方式單個事務的執行時間太長,鎖的時間長,大事務會導致主從延遲。第三種方式會造成鎖衝突,如果能先獲取到所有的id,然後分段,使用第三種方式也可以。
Next-Key Lock
-
如果事務T1已經通過next-key locking鎖定了如下範圍
(10,11]、(11,13]
當插入新的記錄12時,則鎖定的範圍會變成
(10,11]、(11,12]、(12,13]
-
當查詢的索引爲唯一索引時,存儲引擎會對Next-Key Lock進行優化,將其降級爲Record Lock,即僅鎖住索引本身,而不是範圍,從而提高應用的併發性。
DROP TABLE IF EXISTS `t_test`; CREATE TABLE `t_test` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; BEGIN; INSERT INTO `t_test` VALUES (1); INSERT INTO `t_test` VALUES (2); INSERT INTO `t_test` VALUES (4); COMMIT;
時間 Session1 Session2 BEGIN; select * from t_test
where id=4 for update;BEGIN; INSERT INTO t_test
VALUES (3);
成功執行,沒有阻塞COMMIT; COMMIT; 表t_test的a列建立了唯一索引,有1、2、4三個值。在Session1種對id=1進行了X鎖定。因爲id是唯一索引,所以鎖定的不是(2,4)這個範圍,而是2,這樣在Session2種插入3不會阻塞,可以立即插入並返回,即降級爲Record Lock
鎖實驗
- MySql只有在RR的隔離級別下才有gap lock和next-key lock
- 加鎖規則
- 規則1:加鎖的基本單位是next-key lock(前開後閉區間)
- 規則2:查找過程中訪問到的對象纔會加鎖
- 優化1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock退化爲Record Lock(行鎖)
- 優化2:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,next-key lock退化爲Gap Lock(間隙鎖)
- 默認情況下,InnoDB工作在RR級別下,並且會以Next-Key Lock的方式對數據行進行加鎖,這樣可以有效防止幻讀的發生。以下所有的實驗都是在RR級別下。
準備數據
-
建表
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(0,0,0), (5,5,5), (10,10,10), (15,15,15), (20,20,20), (25,25,25);
-
數據
+----+------+------+ | id | c | d | +----+------+------+ | 0 | 0 | 0 | | 5 | 5 | 5 | | 10 | 10 | 10 | | 15 | 15 | 15 | | 20 | 20 | 20 | | 25 | 25 | 25 | +----+------+------+
等值查詢間隙鎖
-
等值查詢間隙鎖
-
分析
- 由於表t種沒有id=7的記錄,所以根據規則1,加鎖單位是next-key lock,Session1加鎖範圍就是(5,10],因爲id=7是一個等值查詢,根據優化2,id=10不滿足條件,next-key lock退化成Gap Lock,因此最終加鎖的範圍是(5,10)
- Session2要向這個間隙裏插入id=8的記錄是必須等到Session1的事務提交以後纔可以
- Session3修改id=10,這個不在加鎖範圍,所以不會阻塞
非唯一索引等值鎖
-
非唯一索引等值鎖
-
分析
-
Session1給索引c上的c=5加上讀鎖,根據規則1,加鎖單位爲next-key lock,因爲c是普通索引,所以訪問c=5後,還需要向右遍歷,一直到c=10停止,根據原則2,訪問到的都要加鎖,所以加鎖範圍是(5,10]。根據優化2,等值查詢,退化爲Gap Lock,因此最終加鎖範圍(5,10)
-
Session2要向這個間隙裏插入id=7的記錄是必須等到Session1的事務提交以後纔可以,因爲session 1的間隙鎖範圍是(5,10)
-
根據原則2,只有訪問到的對象纔會加鎖,這個查詢使用覆蓋索引,並不需要訪問主鍵索引,所以主鍵索引上沒有加任何鎖。所以Session3的語句可以執行成功,不會阻塞
-
-
在這個例子中,lock in share mode只鎖覆蓋索引,但是如果是for update。執行 for update時,系統會認爲你接下來要更新數據,因此會順便給主鍵索引上滿足條件的行加上行鎖。
-
如果你要用lock in share mode來給行加讀鎖避免數據被更新的話,就必須得繞過覆蓋索引的優化,在查詢字段中加入索引中不存在的字段。比如,將session A的查詢語句改成select d from t where c=5 lock in share mode。
非唯一索引等值鎖for Update
-
新的數據準備
DROP TABLE IF EXISTS `t`; CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_a` (`a`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; BEGIN; INSERT INTO `t` VALUES (2, 1); INSERT INTO `t` VALUES (3, 3); INSERT INTO `t` VALUES (4, 5); INSERT INTO `t` VALUES (5, 8); INSERT INTO `t` VALUES (6, 11); COMMIT;
-
因爲表t在a列有索引,所以索引可能被鎖住的範圍爲
(-∞, 1], (1, 3], (3, 5], (5, 8], (8, 11], (11, +∞)
-
非唯一索引等值鎖for update
-
Session1執行後會鎖住索引行的範圍爲
(5, 8], (8, 11]
即鎖住了8所在的範圍,還鎖住了下一個範圍,即Next-Key。所以插入12和4都不會阻塞,這個很好理解,但是爲什麼11不會阻塞,5卻阻塞了?似乎與預期並不符合(預期是**(5,11]**之間所有的都會阻塞纔對,而5應該不會阻塞纔對)
-
先來看看插入5爲什麼會阻塞,如下圖所示,在索引a上的等值查詢(a=8),給索引a加上了next-key(5, 8], (8, 11],Session1語句的for update會給聚集索引(id=8)加上行鎖(黃色顯示部分).
Session2插入5的圖,因爲索引是有序的,並且非聚集索引的葉子節點中的數據是順序存放的,所以對於聚集索引來說,行鎖只鎖住(id=5,a=8)行索引,所以插入是沒問題的,但是對於非聚集索引來說(a=5,id=4)之後是無法插入數據(a=5,id=7)的,因爲鎖的範圍是**( (a=5,id=4), (a=8,id=5) ], ( (a=8,id=5), (a=11,id=6) ]**,(a=5,id=7)在鎖的範圍裏,即無法插入,也就是說只要(a=5,id>4)都是無法插入的.
Session2插入11的圖,因爲索引是有序的,所以對於聚集索引來說,行鎖只鎖住(id=5,a=8)行索引,所以插入是沒問題的,對於非聚集索引來說(a=11,id=6)之後是插入數據(a=11,id=7)也是沒問題的,因爲(a=11,id=7)不在鎖的範圍**( (a=5,id=4), (a=8,id=5) ], ( (a=8,id=5), (a=11,id=6) ]****裏,即(a=11,id>6)都是可以插入的
- 這裏有一個擴展,如果執行以下SQL會不會阻塞呢?
insert into t(id,a) values(1,5);
其實是不會阻塞的,根據上面的分析,索引是有序的,(a=5,id=1)不在(a=5,id>4)範圍,所以不會阻塞
主鍵索引範圍鎖
-
對於以下兩條sql,加鎖範圍不一樣,第一條是id=10的行鎖,第二條是id=10的行鎖和(10,15]的next-key lock
select * from t where id=10 for update; select * from t where id>=10 and id<11 for update;
-
主鍵索引範圍鎖
-
分析
- Session1根據規則1,加鎖單位爲next-key lock,因爲是id=10等值查詢,所以退化爲行鎖,id<11範圍查找繼續找,一直到id=15停止,因此next-key lock(10,15]。最終Session1在主鍵索引的加鎖範圍行鎖id=10和next-key lock(10,15]
- Session2和Session3的分析與上面的一樣
非唯一索引範圍鎖
-
非唯一索引範圍鎖
-
分析:Session1索引c上的(5,10] 和(10,15] 這兩個next-key lock