天天看點

MySQL 入門(4):鎖

MySQL 入門(4):鎖

摘要

在這篇文章中,我将從上一篇的一個小例子開始,跟你介紹一下InnoDB中的行鎖。

在這裡,會涉及到一個概念:兩階段加鎖協定。

之後,我會介紹行鎖中的S鎖和X鎖,以及這兩種鎖的作用。

但是我們會發現僅僅有行鎖是不能解決幻讀問題的,于是我會用例子的方式跟你介紹各種間隙鎖。

最後,我會聊一聊粒度更大的表級鎖和庫鎖。

1 行鎖

在上一篇的文章中,我們用了這個具體的例子來解釋MVCC:

假設我們調換一下T5和T6:

此時,T5是沒有辦法執行的。

原因是這樣的:InnoDB在更新一行的時候,需要先擷取這一行的行鎖。

但是,當一條語句擷取了行鎖之後,不是這行語句執行完畢就能釋放鎖,而是要等到這個事務執行完畢,才會釋放鎖。

這裡涉及到了兩階段加鎖協定:它規定事務的加鎖和解鎖分為兩個獨立的階段,加鎖階段隻能加鎖不能解鎖,一旦開始解鎖,則進入解鎖階段,不能再加鎖。

然後我們再來說說共享鎖(S鎖,讀鎖)和排他鎖(X鎖,寫鎖)。

對于共享鎖來說,如果一個事務擷取了某一行的共享鎖,則這個事務隻能讀這一行資料,而不能修改,并且其他事務也可以擷取這一行資料的共享鎖,讀取這一行的資料,同樣不能修改資料。

對于排它鎖,隻能被某一個事務擷取。并且在擷取排它鎖之前,這一行資料上不能存在共享鎖。一旦某一個事務擷取了這一行的排它鎖,那麼隻有這一個事務可以對這一行資料進行讀寫操作,其他事務對這一行資料的讀寫操作都會被阻塞。

此外,不僅僅隻有更新操作,插入、删除操作也會擷取這一行資料的X鎖。

在這裡我還要再介紹這兩個概念:“快照讀”和“目前讀”。

你可能還會有印象,在上一篇内容中,我提到了所有的更新操作都必須是“目前讀”,現在可以解釋原理了,在更新一行資料的時候,InnoDB會對需要更新的那行資料加上X鎖,直接擷取最新的那一行資料。

與之相對的是“快照讀”,也就是MVCC中的資料讀取方式,利用“快照”來讀取資料的方式,可以極大的提高事務的并發度。

但是并不是說select語句就隻能讀取快照,它也照樣可以給需要讀取的資料加鎖,來讀取最新的資料。也就是說,select語句也一樣可以“目前讀”。

下面這兩個select語句,就是分别加了讀鎖(S鎖,共享鎖)和寫鎖(X鎖,排他鎖)。

mysql> select k from t where id=1 lock in share mode;

mysql> select k from t where id=1 for update;

注意,由于兩階段加鎖協定的存在,如果你采用了一緻性讀,那麼這個鎖必須要等事務送出後才能解除。這是犧牲了并發度的一種做法。是以,如果所有的select語句,都加上了S鎖,此時的“可重複讀”,就變成了“序列化”。

2 間隙鎖

2.1 幻讀問題

還記得我們上面提到過的幻讀嗎?

現在你應該能夠了解幻讀産生的原因了:因為在插入資料的時候,InnoDB采用的是目前讀,而讀取資料的時候,由于MVCC的存在,采用的是快照讀,這就造成了幻讀。

但是我們在上面又提到了,select語句也一樣可以采用“目前讀”。那麼,這樣能解決幻讀嗎?

答案是能解決其中一種情況的幻讀。

比如我們在上一篇文章中舉的關于幻讀的例子:

現在你能了解了,因為這裡的select是快照讀,而事務B的插入操作對于事務A來說是不可見的。如果在T5時刻,事務A的sql語句是select * from t where v = 0 for update,即采用目前讀的話,是可以看得到事務B所送出的資料的,這樣的話,就避免了幻讀的情況。

那如果在T2時刻,事務A的語句就是select * from t where v = 0 for update會怎麼樣的?

如果在T2時刻就使用了“目前讀”,那麼T3時刻事務B是無法進行插入操作的。你可以了解為,T2時刻,InnoDB把v=0的資料,都給加上了一把鎖。

因為這行sql語句把v=0的資料行都鎖住了,是以沒有辦法再插入一行v=0的資料。

這聽起來似乎沒什麼不對的,但是你仔細想一想,InnoDB中的行鎖,鎖住的是已經存在的資料。而對于即将要插入的資料,為什麼也會被鎖住呢?這是不符合行鎖的定義的。

這個時候就可以說到間隙鎖了。

簡單來講,就是這條語句不僅會鎖住所查詢的那行資料,還會把這行資料周圍的間隙鎖住,不讓其他事務插入。

也就是說,行鎖是鎖住已有的資料,而間隙鎖,是鎖住即将要插入的位置,不讓其他資料插入。

在官方文檔有這麼一句話:

Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED or enable the innodb_locks_unsafe_for_binlog system variable (which is now deprecated).

也就是說,間隔鎖在“可重複讀”事務隔離級别是預設生效的。是以,MySQL在“可重複讀”的事務隔離級别下,是有辦法解決幻讀問題的。

下面我們來看看哪些情況InnoDB會給資料加上間隔鎖,并且這裡的間隔鎖範圍有多大,注意,下面列舉的四種情況,指的是where條件中的字段的索引類型。

主鍵索引

唯一普通索引

非唯一普通索引

無索引

先定義這麼一個表:

CREATE TABLE

t

(

id

int(11) NOT NULL,

a

int(11) DEFAULT NULL,

b

c

PRIMARY KEY (

id

),

UNIQUE KEY

a

a

KEY

b

b

)

) ENGINE=InnoDB;

id是主鍵,a是一個唯一索引,b是一個普通索引,c不包含任何的索引字段。

然後插入以下的這些資料:

insert into t values(0,0,0,0),(5,5,5,5),(10,10,10,10);

然後我們開始分析各種情況。

2.2 主鍵索引

因為沒有其他的資料,是以主鍵索引在資料頁内的編排如上圖,并且含有4個空隙。這裡說的“空隙”,指的是資料可以插入的位置。

比如我要插入一個id為3的資料,這條資料就會插入到位于(0,5)這個空隙内。

下面我們開始嘗試:

毫無疑問T3時刻的sql語句是會被阻塞的,原因是id = 5的這行資料已經被加鎖了。那麼,會不會存在有間隙鎖呢?

因為這是一個主鍵索引,InnoDB必須保證id = 5的資料是唯一的,是以對于id=5的周圍,比如(0,5)和(5,10),不需要再加間隙鎖了。

那麼換一個條件再試試,我們查找id大于6且id小于8的資料,此時事務B中的語句同樣會被阻塞。

這是因為,在主鍵索引沒有命中的時候,會對所在的空白範圍,全部加鎖。注意,我這裡說的是未命中的所有空白範圍,哪怕我這裡的查找條件是大于6且小于8,但是加鎖的範圍不是(6,8),而是(5,10)。

你可以簡單的了解為:從查找條件的最小值開始,往前找到第一個索引值;并且從查找條件的最大值開始,往後找到第一個索引值,這個範圍就是加鎖的範圍。

你可能還會有一個疑問,如果是select * from t where id = 8 for update會怎麼樣呢?這個問題和上面一樣,隻要未命中,就加範圍鎖,鎖住空隙(5,10)。

總結一下:對于主鍵索引來說,命中了,就隻加行鎖;沒命中,則對查找範圍的最小值往前找第一個主鍵,查找範圍的最大值往後找第一個主鍵,并對這個範圍加上間隙鎖。

2.3 唯一索引

對于唯一索引來說,和主鍵索引其實是差不多的。當索引命中之後,因為唯一索引同樣保證了索引的唯一性,是以不需要給這行資料的周圍加上間隙鎖,隻會給命中的資料加鎖。

但是這裡和主鍵索引不同的地方是,在給唯一索引a = 5加鎖的同時,還會回表,将a = 5對應的主鍵id = 5這行記錄加鎖。是以,事務B的修改也同樣會被阻塞。

這也是為了防止造成資料不一緻的情況,比如我把a = 5的這行資料删了,然後事務B又通過這行資料的主鍵來對這行資料進行操作。

對于帶有範圍的查找,和上面主鍵索引的間隙鎖規則是一樣的,這裡不再贅述。值得注意的是,在唯一索引中,隻要命中了,就會相應的給這條索引對應的主鍵id也加鎖。

還需要補充一點,當主鍵索引和唯一索引直接命中的時候,如下圖所示,InnoDB除了給a = 5這行資料加了行鎖,還可能給(5, 5)這個間隙加了間隙鎖,這樣的說法聽起來很奇怪。

因為事務A是給a = 5這行資料加了行鎖,而行鎖隻能針對已經存在的資料,不能加到即将插入的資料上;此外,當事務A執行這條語句的時候,事務B是會被阻塞的。直到事務A送出,事務B才會提示唯一索引重複。也就是說,在事務B執行這行語句的時候,是無法通路id = 5這行資料的,事務B不知道id = 5到底存不存在。

是以我才說:當索引直接命中的時候,還會加上這麼一個小小的間隙鎖。我沒有查到這方面的資料,如果你能解釋的話,請留言告訴我。

2.4 普通索引

對于普通索引來說,與唯一索引最大的差別,就是普通索引不是必須唯一的,也就是說,當插入資料的時候,可能會有重複的情況。

而在上面的内容中我們也發現了一個規律:InnoDB的間隙鎖,就是為了防止新插入的資料影響查找結果。

是以對于普通索引來說,還需要防止新插入的資料和原資料一樣的情況(因為唯一索引不需要擔心這麼一種情況)。

下面我們舉例說明,在此之前先插入一行資料:

insert into t values(8,8,5,8);

那麼此時我們的索引b,是這樣的:

因為是非唯一索引的原因,在兩個b = 5的間隙,也能插入資料。

如圖所示,我們這次把查找條件換成了b = 5。此時,我們插入的資料id = 1,理論上應該要插入(0,5)這個間隙内,但是由于間隙鎖的存在,插入将被阻塞。

換一句話說,隻要此時插入的資料b = 5,那麼就一定無法插入。

而對于未命中的條件,規則和上文中說到的一樣,根據查找條件的最小值往前找到第一個一個索引,再根據這個條件的最大值往後找到第一個索引,構成間隙鎖的範圍。

此外,與唯一索引一樣,所有命中的資料行,都會回表将主鍵id也鎖住。

2.5 無索引

可以看到,我們的查找條件是c = 5,直接命中了資料。此時我們插入的資料是c = 6,看起來和事務A無關,但是出乎意料的是,事務B還是會被阻塞。

直接說結論:對于不含有索引的查找項來說,會鎖住所有的間隙和所有的資料。

關于幻讀的問題的一些case,到這裡就研究完了(但是我不确定有沒有遺漏,如果有,還請你留言告訴我)。

在最後還需要說一個概念,行鎖與間隔鎖,合稱next-key lock。并且需要注意的是,隻有在可重複讀的事務隔離級别中,才會有間隔鎖。并且可重複讀是遵循兩階段鎖協定,所有加鎖的資源,都是在事務送出或者復原的時候才釋放的。是以,在防止幻讀産生的時候,同樣降低了并發度。

3 表級鎖

在上一節說完了行級鎖之後,我們再來聊聊表級鎖。

表級鎖有兩種,一種是顯式添加的,一種是隐式添加的。

3.1 讀寫表鎖

還記得我們在上文中提到的讀鎖和寫鎖的特點嗎,這點在表鎖中是一樣的。

給表加上了寫鎖,意味着隻有這個會話擁有讀寫這個表的權限;給表加上了讀鎖,才能讀取這個表上的資料,并且可以多個線程共享讀鎖,但是,隻有當某個表上沒有讀鎖時,才能給這個表加上寫鎖。

下面是給表加鎖的文法:

lock tables table_name read

lock tables table_name write

3.2 MDL

MDL指的是(Metadata Lock),指的是中繼資料鎖。

MDL也分為了讀鎖和寫鎖,功能和上面提到的一樣。

隻不過MDL不需要像表鎖那樣顯式的使用,它會在通路一個表的時候會被自動加上。其中,在某個表對資料進行操作(包括insert,delete,update,select)的時候,會隐式的加上MDL讀鎖,在修改表的結構的時候,會加上寫鎖。

這樣做的目的是,防止在一個事務操作資料的時候,表結構被另一個事務給修改了。或者在某一個事務修改表結構的時候,不允許其他的事務操作資料。

4 庫鎖

顧名思義,庫鎖就是對整個資料庫執行個體加鎖。

MySQL提供了一個加全局讀鎖的方法,指令是Flush tables with read lock (FTWRL)。

使用過這個指令之後,相當于對全庫增加了一個讀鎖,此時其他線程的資料更新語句(資料的增删改)、資料定義語句(包括建表、修改表結構等)和更新類事務的送出語句都會被阻塞。

全局鎖的典型使用場景是,做全庫邏輯備份。當然了,實作這個功能,我們也可以使用“可重複讀”的事務隔離級别,做一次快照讀,依然可以實作備份的功能。隻不過,有些引擎并沒有實作這個事務隔離級别。

寫在最後

首先,謝謝你能看到這裡。

在這篇文章中,尤其是間隙鎖部分的内容,我沒有查到太多的資料,是以很多内容都是我自己的了解。是以如果你發現了一些bad case,請你留言告訴我。又或者你發現了我哪裡的了解是不對的,也請你留言告訴我,謝謝!

當然了,如果有哪裡是我講的不夠明白的,也歡迎留言交流~

原文位址

https://www.cnblogs.com/hongjijun/p/12880218.html