MySQL鎖(三)行鎖:幻讀是什麼?如何解決幻讀?

概述

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

MySQL的行鎖是各個引擎內部實現的,不是全部的引擎支持行鎖,例如MyISAM就不支持行鎖。mysql

不支持行鎖就意味着在併發操做時,就要使用表鎖,在任意時刻都只能有一個更新操做在執行,這樣會影響業務的併發性。這也是爲何MyISAM會被InnoDB取代的緣由之一。算法

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

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

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

什麼是幻讀?

咱們看一下這個例子,有一個表 t,插入部分數據。session

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,都是「當前讀」,符合數據可見性規則。3d

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

不,這裏還真就有問題。

幻讀有什麼問題?

語義上不一致

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,至關於加了表鎖。

參考資料

相關文章
相關標籤/搜索