6. 意向鎖
6.1. 背景
前面提到的S
鎖和X
鎖的語(yǔ)法規(guī)則其實(shí)是針對(duì)記錄的,也就是行鎖,原因是InnoDB中行鎖用的最多。如果將鎖的粒度和鎖的基本模式排列組合一下,就會(huì)出現(xiàn)如下4種情況:
- 行級(jí)
S
鎖 - 行級(jí)
X
鎖 - 表級(jí)
S
鎖 - 表級(jí)
X
鎖
那么接下來(lái)的描述,也就順理成章了。
如果事務(wù)給一個(gè)表添加了表級(jí)S
鎖,則:
- 其他事務(wù)可以繼續(xù)獲得該表的
S
鎖,但是無(wú)法獲取該表的X
鎖; - 其他事務(wù)可以繼續(xù)獲得該表某些行的
S
鎖,但是無(wú)法獲取該表某些行的X
鎖。
如果事務(wù)給一個(gè)表添加了表級(jí)X
鎖,則:
- 不論是該表的
S
鎖、X
鎖,還是該表某些行的S
鎖、X
鎖,其他事務(wù)都只能干瞪眼兒,啥也獲取不了。
挺好理解的吧,總之就是 S鎖只能和S鎖相容,X鎖和其他任何鎖都互斥 。問(wèn)題來(lái)了,雖然用的不多,但是萬(wàn)一我真的想給整個(gè)表添加一個(gè)S
鎖或者X
鎖怎么辦?
假如我要給表user
添加一個(gè)S
鎖,那就必須保證user
在表級(jí)別上和行級(jí)別上都不能有X
鎖,表級(jí)別上還好說(shuō)一點(diǎn),無(wú)非就是1個(gè)內(nèi)存結(jié)構(gòu)罷了,但是行X
鎖呢?必須得逐行遍歷是否有行X
鎖嗎?
同理,假如我要給表user
添加一個(gè)X
鎖,那就必須保證user
在表級(jí)別上和行級(jí)別上都不能有任何鎖(S
和X
都不能有),難不成得逐行遍歷是否有S
或X
鎖嗎?
遍歷是不可能遍歷的!這輩子都不可能遍歷的!于是, 意向鎖 (Intension Lock)誕生了。
6.2. 概念
我們要避免遍歷,那最好的辦法就是在給行加鎖時(shí),先在表級(jí)別上添加一個(gè)標(biāo)識(shí)。
- 意向共享鎖(Intension Shared Lock):簡(jiǎn)稱(chēng)
IS
鎖,當(dāng)事務(wù)試圖給行添加S
鎖時(shí),需要先在表級(jí)別上添加一個(gè)IS
鎖; - 意向排他鎖(Intension Exclusive Lock):簡(jiǎn)稱(chēng)
IX
鎖,當(dāng)事務(wù)試圖給行添加X
鎖時(shí),需要先在表級(jí)別上添加一個(gè)IX
鎖。
這樣一來(lái):
- 如果想給
user
表添加一個(gè)S
鎖(表級(jí)鎖),就先看一下user
表有沒(méi)有IX
鎖;如果有,就說(shuō)明user
表的某些行被加了X
鎖(行鎖),需要等到行的X
鎖釋放,隨即IX
鎖被釋放,才可以在user
表中添加S
鎖; - 如果想給
user
表添加一個(gè)X
鎖(表級(jí)鎖),就先看一下user
有沒(méi)有IS
鎖或IX
鎖;如果有,就說(shuō)明user
表的某些行被加了S
鎖或X
鎖(行鎖),需要等到所有行鎖被釋放,隨即IS
鎖或IX
鎖被釋放,才可以在user
表中添加X
鎖。
需要注意的是,意向鎖和意向鎖之間是不沖突的,意向鎖和行鎖之間也不沖突。
只有在對(duì)表添加
S
鎖或X
鎖時(shí)才需要判斷當(dāng)前表是否被添加了IS
鎖或IX
鎖,當(dāng)為表添加IS
鎖或IX
鎖時(shí),不需要關(guān)心當(dāng)前表是否已經(jīng)被添加了其他IS
鎖或IX
鎖。
目前為止MySQL鎖的基本模式就介紹完了,接下來(lái)回到這片文章的題目,MySQL鎖,鎖住的到底是什么?由于InnoDB的行鎖用的最多,這里的鎖自然指的是行鎖。
7. 行鎖的原理
既然都叫行鎖了,我們姑且猜測(cè)一下,行鎖鎖住的是一行數(shù)據(jù)。我們做個(gè)實(shí)驗(yàn)。
7.1. 沒(méi)有任何索引的表
我們先創(chuàng)建一張沒(méi)有任何索引的普通表,語(yǔ)句如下
CREATE TABLE `user_t1` (
`id` int DEFAULT NULL,
`name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表中數(shù)據(jù)如下:
mysql> SELECT * FROM user_t1;
+------+-------------+
| id | name |
+------+-------------+
| 1 | chanmufeng |
| 2 | wanggangdan |
| 3 | wangshangju |
| 4 | zhaotiechui |
+------+-------------+
接下來(lái)我們?cè)趦蓚€(gè)session中開(kāi)啟兩個(gè)事務(wù)。
- 事務(wù)1,我們通過(guò)
WHERE id = 1
“鎖住”第1行數(shù)據(jù); - 事務(wù)2,我們通過(guò)
WHERE id = 2
"鎖住"第2行數(shù)據(jù)。
一件詭異的事情是,第2個(gè)加鎖的操作被阻塞了。實(shí)際上,T2
中不管我們要給user_t1
中哪行數(shù)據(jù)加鎖,都會(huì)失敗!
為什么我SELECT
一條數(shù)據(jù),卻給我鎖住了整個(gè)表?這個(gè)實(shí)驗(yàn)直接推翻了我們的猜測(cè), InnoDB的行鎖并非直接鎖定Record行 。
為什么沒(méi)有索引的情況下,給某條語(yǔ)句加鎖會(huì)鎖住整個(gè)表呢?別急,我們繼續(xù)。
7.2. 有主鍵索引的表
我們?cè)賱?chuàng)建一個(gè)表user_t2
,語(yǔ)句如下:
CREATE TABLE `user_t2` (
`id` int NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
和user_t1
的不同之處在于為id
創(chuàng)建了一個(gè)主鍵索引。表中數(shù)據(jù)依然如下:
mysql> SELECT * FROM user_t2;
+------+-------------+
| id | name |
+------+-------------+
| 1 | chanmufeng |
| 2 | wanggangdan |
| 3 | wangshangju |
| 4 | zhaotiechui |
+------+-------------+
同樣開(kāi)啟兩個(gè)事務(wù):
- 事務(wù)1,通過(guò)
WHERE id = 1
“鎖住”第1行數(shù)據(jù); - 事務(wù)2
- 依然使用
WHERE id = 1
嘗試加鎖,加鎖失敗; - 使用
WHERE id = 2
嘗試加鎖,加鎖成功。
- 依然使用
既然鎖的不是Record行,難不成鎖的是id
這一列嗎?
我們?cè)僮鲎詈笠粋€(gè)實(shí)驗(yàn)。
7.3. 有唯一索引的表
我們?cè)賱?chuàng)建一個(gè)表user_t3
,語(yǔ)句如下:
CREATE TABLE `user_t3` (
`id` int NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY (`uk_name`) (`name`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
和user_t2
的不同之處在于為name
列創(chuàng)建了一個(gè)唯一索引。表中數(shù)據(jù)依然如下:
mysql> SELECT * FROM user_t3;
+------+-------------+
| id | name |
+------+-------------+
| 1 | chanmufeng |
| 2 | wanggangdan |
| 3 | wangshangju |
| 4 | zhaotiechui |
+------+-------------+
兩個(gè)事務(wù):
- 事務(wù)1,通過(guò)
name
字段 “鎖住”name
為“chanmufeng”的數(shù)據(jù); - 事務(wù)2
- 依然使用
WHERE name = “chanmufeng”
嘗試加鎖,可以預(yù)料,加鎖失敗; - 使用
WHERE id = 1
嘗試給同樣的行加鎖,加鎖失敗。
- 依然使用
通過(guò)3個(gè)實(shí)驗(yàn)我們發(fā)現(xiàn),行鎖鎖住的既不是Record行,也不是Column列,那到底鎖住的是什么?我們對(duì)比一下,上文的3張表的不同點(diǎn)在于索引不同,其實(shí) InnoDB的行鎖,就是通過(guò)鎖住索引來(lái)實(shí)現(xiàn)的 。
接下來(lái)回答3個(gè)問(wèn)題。
8. 三個(gè)問(wèn)題
8.1. 鎖住索引?沒(méi)有索引怎么辦?
你說(shuō)鎖住索引?如果我不創(chuàng)建索引,MySQL鎖定個(gè)啥?
如果我們沒(méi)有設(shè)置主鍵,InnoDB會(huì)優(yōu)先選取一個(gè)不包含NULL值的Unique鍵
作為主鍵,如果表中連Unique鍵
也沒(méi)有的話(huà),就會(huì)自動(dòng)為每一條記錄添加一個(gè)叫做DB_ROW_ID
的列作為默認(rèn)主鍵,只不過(guò)這個(gè)主鍵我們看不到罷了。
下圖是數(shù)據(jù)的行格式。看不懂的話(huà)強(qiáng)烈推薦看一下我上面給出的兩篇文章,說(shuō)得非常明白。
行格式
8.2. 為什么第一個(gè)實(shí)驗(yàn)會(huì)鎖表?
因?yàn)?code>SELECT沒(méi)有用到索引,會(huì)進(jìn)行全表掃描,然后把DB_ROW_ID
作為默認(rèn)主鍵的聚簇索引都給鎖住了。
8.3. 為什么通過(guò)唯一索引給數(shù)據(jù)加鎖,主鍵索引也會(huì)被鎖住?
不管是Unique
索引還是普通索引,它們的葉子結(jié)點(diǎn)中存儲(chǔ)的數(shù)據(jù)都不完整,其中只是存儲(chǔ)了作為索引并且排序好的列數(shù)據(jù)以及對(duì)應(yīng)的主鍵值。
因此我們通過(guò)索引查找數(shù)據(jù)數(shù)據(jù)實(shí)際上是在索引的B+樹(shù)中先找到對(duì)應(yīng)的主鍵,然后根據(jù)主鍵再去主鍵索引的B+樹(shù)的葉子結(jié)點(diǎn)中找到完整數(shù)據(jù),最后返回。所以雖然是兩個(gè)索引樹(shù),但實(shí)際上是同一行數(shù)據(jù),必須全部鎖住。
下面給了一張圖,讓不了解索引的朋友大致了解一下。上半部分是name
列創(chuàng)建的唯一索引的B+樹(shù),下半部分是主鍵索引(也叫聚簇索引)。
假如我們通過(guò)WHERE name = '王鋼蛋'
對(duì)數(shù)據(jù)進(jìn)行查詢(xún),會(huì)先用到name
列的唯一索引,最終定位到主鍵值為1
,然后再到主鍵索引中查詢(xún)id = 1
的數(shù)據(jù),最終拿到完整的行數(shù)據(jù)。
這兩張圖在我索引文章中都有哦~
MySQL鎖-索引
9. 總結(jié)
至此,我已經(jīng)回答了文章開(kāi)頭的絕大多數(shù)問(wèn)題。
MySQL鎖,是解決資源競(jìng)爭(zhēng)問(wèn)題的一種手段。有哪些競(jìng)爭(zhēng)呢?讀—寫(xiě)/寫(xiě)—讀,寫(xiě)—寫(xiě)中都會(huì)出現(xiàn)資源競(jìng)爭(zhēng)問(wèn)題,不同的是前者可以通過(guò)MVCC的方式來(lái)解決,但是某些情況下你也不得不用鎖,因此我也順便解釋了鎖和MVCC的關(guān)系。
然后介紹了MySQL鎖的基本模式,包括共享鎖(S
鎖)和排他鎖(X
鎖),還引入了意向鎖。
最后解釋了鎖到底鎖的是什么的問(wèn)題。通過(guò)3個(gè)實(shí)驗(yàn),最終解釋了InnoDB鎖本質(zhì)上鎖的是索引。
-
計(jì)算機(jī)
+關(guān)注
關(guān)注
19文章
7494瀏覽量
87965 -
MySQL
+關(guān)注
關(guān)注
1文章
809瀏覽量
26575 -
MVCC
+關(guān)注
關(guān)注
0文章
13瀏覽量
1470
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論