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` int(11) DEFAULT NULL,
  `c` int(11) DEFAULT NULL,
  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,請你留言告訴我。又或者你發現了我哪里的理解是不對的,也請你留言告訴我,謝謝!

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

PS:如果有其他的問題,也可以在公眾號找到我,歡迎來找我玩~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM