1. 程式人生 > 實用技巧 >MySQL被鎖了(五)如何解鎖:解決幻讀的方法和功率demo?

MySQL被鎖了(五)如何解鎖:解決幻讀的方法和功率demo?

概述

前面兩篇文章介紹了MySQL的全域性鎖表級鎖,今天就介紹一下MySQL的行鎖。

MySQL的行鎖是各個引擎內部實現的,不是所有的引擎支援行鎖,例如MyISAM就不支援行鎖。

不支援行鎖就意味著在併發操作時,就要使用表鎖,在任意時刻都只能有一個更新操作在執行,這樣會影響業務的併發性。這也是為什麼MyISAM會被InnoDB取代的原因之一。

行鎖是鎖裡最小粒度的鎖,InnoDB引擎裡的行鎖的實現演算法有三種:

  • Record Lock:行鎖,鎖住記錄本身
  • Gap Lock:間隙鎖,鎖住某個範圍,但不包括記錄本身
  • Next-Key Lock:Record Lock + Gap Lock,既鎖範圍,又鎖記錄

InnoDB是使用Next-Key Lock來解決幻讀問題的。

什麼是幻讀?

我們看一下這個例子,有一個表 t,插入部分資料。

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);

圖1 假設只在id=5這一行加行鎖

有三個會話併發執行,Session A在T1,T3,T5時刻分別查詢同一個語句,出現不同的結果。其中Q3讀到的id=1這一行的現象,被稱為幻讀。

幻讀,指同一個事務中,兩次相同的查詢操作,得到的結果行數不一樣。

這裡要對“幻讀”做兩點說明:

  1. 在可重複讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的資料的。因此幻讀在“當前讀”下才會出現。
  2. 上面的Session B的修改結果,被Session A之後的select語句用“當前讀”看到了,不能稱為幻讀。幻讀僅專指“新插入的行”。

根據資料可見性規則分析,這三個查詢都加了for update,都是“當前讀”,符合資料可見性規則。

這麼看來,好像沒什麼問題,是不是真的沒有問題呢?

不,這裡還真就有問題。

幻讀有什麼問題?

語義上不一致

Session A在T1時刻就聲明瞭,“我要把所有d=5的行鎖住,不準別的事務進行讀寫操作”。而實際上,這個語義被破壞了。

上面的例子可能還看不太出來,我們給Session B和Session C分別加兩個語句,再看看會出現什麼現象。


圖2 假設只在id=5這一行加行鎖--語義被破壞

Session B的第二條語句update t set c = 5 where id=0,語義是“我要把id=0、d=5的這一行的c的值改成了5”。

由於在T1時刻,Session A還只是給t=5這一行加了行鎖,並沒有給id=0這一行加鎖。因此Session B在T2時刻,是可以執行這條語句的。

同理,Session C對id=1這行的修改,一樣是破壞了Q1的加鎖宣告。

資料上不一致

其次是造成資料上不一致。鎖的設計就是為了保證資料一致性的,這裡的一致性除了內部資料在此刻的一致性外,還包含資料和日誌在邏輯上的一致性。


圖 3 假設只在id=5這一行加行鎖--資料一致性問題

我們來分析一下圖3執行完成後,資料庫的資料是什麼:

  1. 經過T1時刻,id=5這一行變成 (5,5,100),當然這個結果最終是在T6時刻正式提交的
  2. 經過T2時刻,id=0這一行變成(0,5,5);
  3. 經過T4時刻,表裡面多了一行(1,5,5);

我們再來看看binlog的內容:

// session B
update t set d=5 where id=0;
update t set c=5 where id=0;

// session C
insert into t values(1,1,5);
update t set c=5 where id=1;

update t set d=100 where d=5;

按照這個語句序列,這三行的結果變成:(0,5,100),(1,5,100),(5,5,100)。

也就是說id=0和id=1這兩行,發生了資料不一致。這個問題很嚴重,是不行的。

那究竟這個資料不一致是怎樣引入的呢?


圖 4 假設掃描到的行都被加上了行鎖

假設我們對掃描到的行都加上行鎖,來看看圖4執行後會出現什麼現象。

  1. 經過T1時刻,id=5這一行變成 (5,5,100),當然這個結果最終是在T6時刻正式提交的
  2. 經過T2時刻,Session B被阻塞,等到T6時刻Session A釋放鎖才能執行;
  3. 經過T4時刻,表裡面多了一行(1,5,5);
  4. 經過T6時刻,id=1這一行變成(1,5,100);

id=1這一行還是出現數據不一致的問題。即使把所有的記錄都加上鎖,還是阻止不了新插入的記錄。

如何解決幻讀?

我們現在知道產生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的“間隙”。因此,為了解決幻讀問題,InnoDB引入了間隙鎖(Gap Lock)。

前面介紹過,間隙鎖,鎖住某個範圍,但不包括記錄本身。比如前面說到的表t,初始化有6條記錄,這就產生了7個間隙。


圖 5 表t主鍵索引上的行鎖和間隙鎖

當你執行select * from t where d=5 for update的時候,就不止是給資料庫中6個記錄加了行鎖,還同時加了7個間隙鎖。這樣就確保了無法再插入新的記錄。

也就是說這時候,在一行行掃描的過程中,不僅給行加上行鎖,還給行兩邊的空隙也加上間隙鎖。

我們回到上面的圖4,再來看看加上間隙鎖後,執行的效果如何。

  1. 經過T1時刻,id=5這一行變成 (5,5,100),當然這個結果最終是在T6時刻正式提交的。因為select * from t where d=6 for update,對6個記錄加了行鎖,同時加了7個間隙鎖。
  2. 經過T2時刻,Session B被阻塞,因為id=0這一行被鎖;
  3. 經過T4時刻,Session C被阻塞,因為主鍵索引上加了間隙鎖(0,5),所以id=1這個值無法被插入;

Session B和Session C都要等待Session A釋放鎖後才能繼續執行,這樣就解決了幻讀的問題。

行鎖保證更新行,間隙鎖保證插入行,而行鎖+間隙鎖=Next-Key Lock,也就是本文開頭說到的,InnoDB是通過Next-Key Lock來解決幻讀問題的。

但是間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,這會影響併發度的。比如上面的select * from t where d=5 for update,相當於加了表鎖。