MySQL 入門(4):鎖
阿新 • • 發佈:2020-05-13
## 摘要
在這篇文章中,我將從上一篇的一個小例子開始,跟你介紹一下InnoDB中的行鎖。
在這裡,會涉及到一個概念:兩階段加鎖協議。
之後,我會介紹行鎖中的S鎖和X鎖,以及這兩種鎖的作用。
但是我們會發現僅僅有行鎖是不能解決幻讀問題的,於是我會用例子的方式跟你介紹各種間隙鎖。
最後,我會聊一聊粒度更大的表級鎖和庫鎖。
## 1 行鎖
在上一篇的文章中,我們用了這個具體的例子來解釋MVCC:
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084427006-2045329860.png)
假設我們調換一下T5和T6:
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084435576-2016057198.png)
此時,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`語句也一樣可以採用“當前讀”。那麼,這樣能解決幻讀嗎?
答案是能解決**其中一種情況**的幻讀。
比如我們在上一篇文章中舉的關於幻讀的例子:
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084454165-972887798.png)
現在你能理解了,因為這裡的`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 主鍵索引
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084509419-1364067079.png)
因為沒有其他的資料,所以主鍵索引在資料頁內的編排如上圖,並且含有4個空隙。這裡說的“空隙”,指的是資料**可以插入的位置**。
比如我要插入一個id為3的資料,這條資料就會插入到位於(0,5)這個空隙內。
下面我們開始嘗試:
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084519187-1481034779.png)
毫無疑問T3時刻的`sql`語句是會被阻塞的,原因是`id = 5`的這行資料已經被加鎖了。那麼,會不會存在有間隙鎖呢?
因為這是一個主鍵索引,InnoDB必須保證`id = 5`的資料是唯一的,所以對於`id=5`的周圍,比如(0,5)和(5,10),不需要再加間隙鎖了。
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084531856-2030819061.png)
那麼換一個條件再試試,我們查詢`id大於6且id小於8`的資料,此時事務B中的語句同樣會被阻塞。
這是因為,在主鍵索引沒有命中的時候,會對所在的空白範圍,全部加鎖。注意,我這裡說的是**未命中的所有空白範圍**,哪怕我這裡的查詢條件是大於6且小於8,但是加鎖的範圍不是(6,8),而是(5,10)。
你可以簡單的理解為:從查詢條件的最小值開始,往前找到第一個索引值;並且從查詢條件的最大值開始,往後找到第一個索引值,這個範圍就是加鎖的範圍。
你可能還會有一個疑問,如果是`select * from t where id = 8 for update`會怎麼樣呢?這個問題和上面一樣,只要未命中,就加範圍鎖,鎖住空隙(5,10)。
總結一下:對於主鍵索引來說,命中了,就只加行鎖;沒命中,則對查詢範圍的最小值往前找第一個主鍵,查詢範圍的最大值往後找第一個主鍵,並對這個範圍加上間隙鎖。
### 2.3 唯一索引
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084541665-1900143075.png)
對於唯一索引來說,和主鍵索引其實是**差不多**的。當索引命中之後,因為唯一索引同樣保證了索引的唯一性,所以不需要給這行資料的周圍加上間隙鎖,只會給命中的資料加鎖。
但是這裡和主鍵索引**不同**的地方是,在給唯一索引`a = 5`加鎖的同時,還會回表,將`a = 5`對應的主鍵`id = 5`這行記錄加鎖。所以,事務B的修改也同樣會被阻塞。
這也是為了防止造成資料不一致的情況,比如我把`a = 5`的這行資料刪了,然後事務B又通過這行資料的主鍵來對這行資料進行操作。
對於帶有範圍的查詢,和上面主鍵索引的間隙鎖規則是一樣的,這裡不再贅述。**值得注意的是**,在唯一索引中,只要命中了,就會相應的給這條索引對應的主鍵id也加鎖。
還需要補充一點,當主鍵索引和唯一索引直接命中的時候,如下圖所示,InnoDB除了給`a = 5`這行資料加了行鎖,還可能給(5, 5)這個間隙加了間隙鎖,這樣的說法聽起來很奇怪。
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084552137-989746012.png)
因為事務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,是這樣的:
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084606169-1075241300.png)
因為是**非唯一索引**的原因,在兩個b = 5的間隙,也能插入資料。
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084613494-202627918.png)
如圖所示,我們這次把查詢條件換成了`b = 5`。此時,我們插入的資料`id = 1`,理論上應該要插入(0,5)這個間隙內,但是由於間隙鎖的存在,插入將被阻塞。
換一句話說,只要此時插入的資料`b = 5`,那麼就一定無法插入。
而對於未命中的條件,規則和上文中說到的一樣,根據查詢條件的最小值往前找到第一個一個索引,再根據這個條件的最大值往後找到第一個索引,構成間隙鎖的範圍。
此外,與唯一索引一樣,所有命中的資料行,都會回表將主鍵id也鎖住。
### 2.5 無索引
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084620479-299079241.png)
可以看到,我們的查詢條件是`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:如果有其他的問題,也可以在公眾號找到我,歡迎來找我玩~
![](https://img2020.cnblogs.com/blog/1998080/202005/1998080-20200513084635660-18694973