在這篇文章中,我將從上一篇的一個小例子開始,跟你介紹一下InnoDB中的行鎖。mysql
在這裏,會涉及到一個概念:兩階段加鎖協議。sql
以後,我會介紹行鎖中的S鎖和X鎖,以及這兩種鎖的做用。數據庫
可是咱們會發現僅僅有行鎖是不能解決幻讀問題的,因而我會用例子的方式跟你介紹各類間隙鎖。bash
最後,我會聊一聊粒度更大的表級鎖和庫鎖。併發
在上一篇的文章中,咱們用了這個具體的例子來解釋MVCC:
ui
假設咱們調換一下T5和T6:spa
此時,T5是沒有辦法執行的。線程
緣由是這樣的:InnoDB在更新一行的時候,須要先獲取這一行的行鎖。3d
可是,當一條語句獲取了行鎖以後,不是這行語句執行完畢就能釋放鎖,而是要等到這個事務執行完畢,纔會釋放鎖。code
這裏涉及到了兩階段加鎖協議:它規定事務的加鎖和解鎖分爲兩個獨立的階段,加鎖階段只能加鎖不能解鎖,一旦開始解鎖,則進入解鎖階段,不能再加鎖。
而後咱們再來講說共享鎖(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鎖,此時的「可重複讀」,就變成了「序列化」。
還記得咱們上面提到過的幻讀嗎?
如今你應該可以理解幻讀產生的緣由了:由於在插入數據的時候,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);
複製代碼
而後咱們開始分析各類狀況。
由於沒有其餘的數據,因此主鍵索引在數據頁內的編排如上圖,而且含有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)。
總結一下:對於主鍵索引來講,命中了,就只加行鎖;沒命中,則對查找範圍的最小值往前找第一個主鍵,查找範圍的最大值日後找第一個主鍵,並對這個範圍加上間隙鎖。
對於惟一索引來講,和主鍵索引實際上是差很少的。當索引命中以後,由於惟一索引一樣保證了索引的惟一性,因此不須要給這行數據的周圍加上間隙鎖,只會給命中的數據加鎖。
可是這裏和主鍵索引不一樣的地方是,在給惟一索引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
到底存不存在。
因此我才說:當索引直接命中的時候,還會加上這麼一個小小的間隙鎖。我沒有查到這方面的資料,若是你能解釋的話,請留言告訴我。
對於普通索引來講,與惟一索引最大的區別,就是普通索引不是必須惟一的,也就是說,當插入數據的時候,可能會有重複的狀況。
而在上面的內容中咱們也發現了一個規律:InnoDB的間隙鎖,就是爲了防止新插入的數據影響查找結果。
因此對於普通索引來講,還須要防止新插入的數據和原數據同樣的狀況(由於惟一索引不須要擔憂這麼一種狀況)。
下面咱們舉例說明,在此以前先插入一行數據:
insert into t values(8,8,5,8);
複製代碼
那麼此時咱們的索引b,是這樣的:
由於是非惟一索引的緣由,在兩個b = 5的間隙,也能插入數據。
如圖所示,咱們此次把查找條件換成了b = 5
。此時,咱們插入的數據id = 1
,理論上應該要插入(0,5)這個間隙內,可是因爲間隙鎖的存在,插入將被阻塞。
換一句話說,只要此時插入的數據b = 5
,那麼就必定沒法插入。
而對於未命中的條件,規則和上文中說到的同樣,根據查找條件的最小值往前找到第一個一個索引,再根據這個條件的最大值日後找到第一個索引,構成間隙鎖的範圍。
此外,與惟一索引同樣,全部命中的數據行,都會回表將主鍵id也鎖住。
能夠看到,咱們的查找條件是c = 5
,直接命中了數據。此時咱們插入的數據是c = 6
,看起來和事務A無關,可是出乎意料的是,事務B仍是會被阻塞。
直接說結論:對於不含有索引的查找項來講,會鎖住全部的間隙和全部的數據。
關於幻讀的問題的一些case,到這裏就研究完了(可是我不肯定有沒有遺漏,若是有,還請你留言告訴我)。
在最後還須要說一個概念,行鎖與間隔鎖,合稱next-key lock
。而且須要注意的是,只有在可重複讀的事務隔離級別中,纔會有間隔鎖。而且可重複讀是遵循兩階段鎖協議,全部加鎖的資源,都是在事務提交或者回滾的時候才釋放的。因此,在防止幻讀產生的時候,一樣下降了併發度。
在上一節說完了行級鎖以後,咱們再來聊聊表級鎖。
表級鎖有兩種,一種是顯式添加的,一種是隱式添加的。
還記得咱們在上文中提到的讀鎖和寫鎖的特色嗎,這點在表鎖中是同樣的。
給表加上了寫鎖,意味着只有這個會話擁有讀寫這個表的權限;給表加上了讀鎖,才能讀取這個表上的數據,而且能夠多個線程共享讀鎖,可是,只有當某個表上沒有讀鎖時,才能給這個表加上寫鎖。
下面是給表加鎖的語法:
lock tables table_name read
lock tables table_name write
複製代碼
MDL指的是(Metadata Lock),指的是元數據鎖。
MDL也分爲了讀鎖和寫鎖,功能和上面提到的同樣。
只不過MDL不須要像表鎖那樣顯式的使用,它會在訪問一個表的時候會被自動加上。其中,在某個表對數據進行操做(包括insert,delete,update,select)的時候,會隱式的加上MDL讀鎖,在修改表的結構的時候,會加上寫鎖。
這樣作的目的是,防止在一個事務操做數據的時候,表結構被另外一個事務給修改了。或者在某一個事務修改表結構的時候,不容許其餘的事務操做數據。
顧名思義,庫鎖就是對整個數據庫實例加鎖。
MySQL提供了一個加全局讀鎖的方法,命令是Flush tables with read lock (FTWRL)
。
使用過這個命令以後,至關於對全庫增長了一個讀鎖,此時其餘線程的數據更新語句(數據的增刪改)、數據定義語句(包括建表、修改表結構等)和更新類事務的提交語句都會被阻塞。
全局鎖的典型使用場景是,作全庫邏輯備份。固然了,實現這個功能,咱們也可使用「可重複讀」的事務隔離級別,作一次快照讀,依然能夠實現備份的功能。只不過,有些引擎並無實現這個事務隔離級別。
首先,謝謝你能看到這裏。
在這篇文章中,尤爲是間隙鎖部分的內容,我沒有查到太多的資料,因此不少內容都是我本身的理解。因此若是你發現了一些bad case,請你留言告訴我。又或者你發現了我哪裏的理解是不對的,也請你留言告訴我,謝謝!
固然了,若是有哪裏是我講的不夠明白的,也歡迎留言交流~
PS:若是有其餘的問題,也能夠在公衆號找到我,歡迎來找我玩~