一、解決并發(fā)事務(wù)帶來問題的兩種基本方式
上一篇文章主要學(xué)習(xí)了事務(wù)并發(fā)執(zhí)行時(shí)可能帶來的各種問題,并發(fā)事務(wù)訪問相同記錄的情況我們大致可以劃分為3種:
-
讀-讀
情況:即并發(fā)事務(wù)相繼讀取相同的記錄,我們需要知道的是讀取操作本身不會(huì)對(duì)記錄有一毛錢影響,并不會(huì)引起什么問題,所以允許這種情況的發(fā)生。 -
寫-寫
情況:即并發(fā)事務(wù)相繼對(duì)相同的記錄做出改動(dòng),我們前邊說過,在這種情況下會(huì)發(fā)生臟寫
的問題,任何一種隔離級(jí)別都不允許這種問題的發(fā)生。所以在多個(gè)未提交事務(wù)相繼對(duì)一條記錄做改動(dòng)時(shí),需要讓它們排隊(duì)執(zhí)行
,這個(gè)排隊(duì)的過程其實(shí)是通過鎖
來實(shí)現(xiàn)的。這個(gè)所謂的鎖
其實(shí)是一個(gè)內(nèi)存中的結(jié)構(gòu)
,在事務(wù)執(zhí)行前本來是沒有鎖的,也就是說一開始是沒有鎖結(jié)構(gòu)和記錄進(jìn)行關(guān)聯(lián)的,如圖所示:
當(dāng)一個(gè)事務(wù)想對(duì)這條記錄做改動(dòng)時(shí),首先會(huì)看看內(nèi)存中有沒有與這條記錄關(guān)聯(lián)的鎖結(jié)構(gòu)
,當(dāng)沒有的時(shí)候就會(huì)在內(nèi)存中生成一個(gè)鎖結(jié)構(gòu)
與之關(guān)聯(lián)。比方說事務(wù)T1要對(duì)這條記錄做改動(dòng),就需要生成一個(gè)鎖結(jié)構(gòu)
與之關(guān)聯(lián):其實(shí)在
鎖結(jié)構(gòu)
里有很多信息,我們現(xiàn)在只把兩個(gè)比較重要的屬性拿了出來:-
trx信息
:代表這個(gè)鎖結(jié)構(gòu)是哪個(gè)事務(wù)生成的 -
is_waiting
:代表當(dāng)前事務(wù)是否在等待
如圖所示,當(dāng)
事務(wù)T1
改動(dòng)了這條記錄后,就生成了一個(gè)鎖結(jié)構(gòu)
與該記錄關(guān)聯(lián),因?yàn)橹皼]有別的事務(wù)為這條記錄加鎖,所以is_waiting
屬性就是false
,我們把這個(gè)場景就稱之為獲取鎖成功,或者加鎖成功,然后就可以繼續(xù)執(zhí)行操作了。在
事務(wù)T1
提交之前,另一個(gè)事務(wù)T2
也想對(duì)該記錄做改動(dòng),那么先去看看有沒有鎖結(jié)構(gòu)
與這條記錄關(guān)聯(lián),發(fā)現(xiàn)有一個(gè)鎖結(jié)構(gòu)
與之關(guān)聯(lián)后,然后也生成了一個(gè)鎖結(jié)構(gòu)
與這條記錄關(guān)聯(lián),不過鎖結(jié)構(gòu)的is_waiting
屬性值為true
,表示當(dāng)前事務(wù)需要等待,我們把這個(gè)場景就稱之為獲取鎖失敗,或者加鎖失敗,或者沒有成功的獲取到鎖,畫個(gè)圖表示就是這樣:在
事務(wù)T1
提交之后,就會(huì)把該事務(wù)生成的鎖結(jié)構(gòu)
釋放掉,然后看看還有沒有別的事務(wù)在等待獲取鎖,發(fā)現(xiàn)了事務(wù)T2
還在等待獲取鎖,所以把事務(wù)T2
對(duì)應(yīng)的鎖結(jié)構(gòu)的is_waiting
屬性設(shè)置為false
,然后把該事務(wù)對(duì)應(yīng)的線程喚醒,讓它繼續(xù)執(zhí)行,此時(shí)事務(wù)T2
就算獲取到鎖了。效果圖就是這樣:
我們總結(jié)一下后續(xù)內(nèi)容中可能用到的幾種說法,以免大家后面混淆:-
不加鎖:
意思就是不需要在內(nèi)存中生成對(duì)應(yīng)的鎖結(jié)構(gòu)
,可以直接執(zhí)行操作。 -
獲取鎖成功,或者加鎖成功:
意思就是在內(nèi)存中生成了對(duì)應(yīng)的鎖結(jié)構(gòu),而且鎖結(jié)構(gòu)的is_waiting
屬性為false
,也就是事務(wù)可以繼續(xù)執(zhí)行操作。 -
獲取鎖失敗,或者加鎖失敗,或者沒有獲取到鎖:
意思就是在內(nèi)存中生成了對(duì)應(yīng)的鎖結(jié)構(gòu)
,不過鎖結(jié)構(gòu)的is_waiting
屬性為true
,也就是事務(wù)需要等待,不可以繼續(xù)執(zhí)行操作。
-
-
讀-寫
或寫-讀
情況:也就是一個(gè)事務(wù)進(jìn)行讀取操作,另一個(gè)進(jìn)行改動(dòng)操作。我們前邊說過,這種情況下可能發(fā)生臟讀
、不可重復(fù)讀
、幻讀
的問題小提示:
幻讀問題的產(chǎn)生是因?yàn)槟硞€(gè)事務(wù)讀了一個(gè)范圍的記錄,之后別的事務(wù)在該范圍內(nèi)插入了新記錄,該事務(wù)再次讀取該范圍的記錄時(shí),可以讀到新插入的記錄,所以幻讀問題準(zhǔn)確的說并不是因?yàn)樽x取和寫入一條相同記錄而產(chǎn)生的,這一點(diǎn)要注意一下在上一篇文章中,我們也知道
SQL標(biāo)準(zhǔn)
規(guī)定不同隔離級(jí)別下可能發(fā)生的問題也不一樣:-
在
READ UNCOMMITTED
隔離級(jí)別下,臟讀
、不可重復(fù)讀
、幻讀
都可能發(fā)生 -
在
READ COMMITTED
隔離級(jí)別下,不可重復(fù)讀
、幻讀可
能發(fā)生,臟讀
不可以發(fā)生 -
在
REPEATABLE READ
隔離級(jí)別下,幻讀
可能發(fā)生,臟讀
和不可重復(fù)讀
不可以發(fā)生 -
在
SERIALIZABLE
隔離級(jí)別下,上述問題都不可以發(fā)生
不過各個(gè)數(shù)據(jù)庫廠商對(duì)
SQL標(biāo)準(zhǔn)
的支持都可能不一樣,與SQL標(biāo)準(zhǔn)不同的一點(diǎn)就是,MySQL在REPEATABLE READ隔離級(jí)別實(shí)際上就已經(jīng)解決了幻讀問題
怎么解決
臟讀
、不可重復(fù)讀
、幻讀
這些問題呢?其實(shí)有兩種可選的解決方案:-
方案一:
讀操作利用多版本并發(fā)控制(MVCC),寫操作進(jìn)行加鎖所謂的
MVCC
我們?cè)谇耙黄恼掠羞^詳細(xì)的描述,就是通過生成一個(gè)ReadView
,然后通過ReadView
找到符合條件的記錄版本(歷史版本是由undo日志構(gòu)建的),其實(shí)就像是在生成ReadView
的那個(gè)時(shí)刻做了一次時(shí)間靜止(就像用相機(jī)拍了一個(gè)快照),查詢語句只能讀到在生成ReadView
之前已提交事務(wù)所做的更改,在生成ReadView
之前未提交的事務(wù)或者之后才開啟的事務(wù)所做的更改是看不到的。而寫操作肯定針對(duì)的是最新版本的記錄,讀記錄的歷史版本和改動(dòng)記錄的最新版本本身并不沖突,也就是采用MVCC
時(shí),讀-寫
操作并不沖突。小提示:
我們說過普通的SELECT語句在READ COMMITTED和REPEATABLE READ隔離級(jí)別下會(huì)使用到MVCC讀取記錄。在READ COMMITTED隔離級(jí)別下,一個(gè)事務(wù)在執(zhí)行過程中每次執(zhí)行SELECT操作時(shí)都會(huì)生成一個(gè)ReadView,ReadView的存在本身就保證了事務(wù)不可以讀取到未提交的事務(wù)所做的更改,也就是避免了臟讀現(xiàn)象;REPEATABLE READ隔離級(jí)別下,一個(gè)事務(wù)在執(zhí)行過程中只有第一次執(zhí)行SELECT操作才會(huì)生成一個(gè)ReadView,之后的SELECT操作都復(fù)用這個(gè)ReadView,這樣也就避免了不可重復(fù)讀和幻讀的問題 -
方案二:
讀、寫操作都采用加鎖的方式如果我們的一些業(yè)務(wù)場景不允許讀取記錄的舊版本,而是每次都必須去讀取記錄的最新版本,比方在銀行存款的事務(wù)中,你需要先把賬戶的余額讀出來,然后將其加上本次存款的數(shù)額,最后再寫到數(shù)據(jù)庫中。在將賬戶余額讀取出來后,就不想讓別的事務(wù)再訪問該余額,直到本次存款事務(wù)執(zhí)行完成,其他事務(wù)才可以訪問賬戶的余額。這樣在讀取記錄的時(shí)候也就需要對(duì)其進(jìn)行
加鎖
操作,這樣也就意味著讀操作和寫操作也像寫-寫操作那樣排隊(duì)執(zhí)行
小提示:
我們說臟讀的產(chǎn)生是因?yàn)楫?dāng)前事務(wù)讀取了另一個(gè)未提交事務(wù)寫的一條記錄,如果另一個(gè)事務(wù)在寫記錄的時(shí)候就給這條記錄加鎖,那么當(dāng)前事務(wù)就無法繼續(xù)讀取該記錄了,所以也就不會(huì)有臟讀問題的產(chǎn)生了。不可重復(fù)讀的產(chǎn)生是因?yàn)楫?dāng)前事務(wù)先讀取一條記錄,另外一個(gè)事務(wù)對(duì)該記錄做了改動(dòng)之后并提交之后,當(dāng)前事務(wù)再次讀取時(shí)會(huì)獲得不同的值,如果在當(dāng)前事務(wù)讀取記錄時(shí)就給該記錄加鎖,那么另一個(gè)事務(wù)就無法修改該記錄,自然也不會(huì)發(fā)生不可重復(fù)讀了。我們說幻讀問題的產(chǎn)生是因?yàn)楫?dāng)前事務(wù)讀取了一個(gè)范圍的記錄,然后另外的事務(wù)向該范圍內(nèi)插入了新記錄,當(dāng)前事務(wù)再次讀取該范圍的記錄時(shí)發(fā)現(xiàn)了新插入的新記錄,我們把新插入的那些記錄稱之為幻影記錄。采用加鎖的方式解決幻讀問題就有那么一丟丟麻煩了,因?yàn)楫?dāng)前事務(wù)在第一次讀取記錄時(shí)那些幻影記錄并不存在,所以讀取的時(shí)候加鎖就有點(diǎn)尷尬 —— 因?yàn)槟悴⒉恢澜o誰加鎖,沒關(guān)系,這難不倒InnoDB,我們稍后揭曉答案,稍安勿躁。
-
很明顯,采用MVCC
方式的話,讀-寫
操作彼此并不沖突,性能更高,采用加鎖
方式的話,讀-寫
操作彼此需要排隊(duì)執(zhí)行,影響性能。一般情況下我們當(dāng)然愿意采用MVCC
來解決讀-寫
操作并發(fā)執(zhí)行的問題,但是業(yè)務(wù)在某些特殊情況下,要求必須采用加鎖的方式執(zhí)行,那也是沒有辦法的事。
1.1 一致性讀(Consistent Reads)
事務(wù)利用MVCC
進(jìn)行的讀取
操作稱之為一致性讀
,或者一致性無鎖讀
,有的地方也稱之為快照讀
。所有普通的SELECT
語句(plain SELECT
)在READ COMMITTED
、REPEATABLE READ
隔離級(jí)別下都算是一致性讀,比如:
SELECT * FROM t;
SELECT * FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2
我們需要知道的是,一致性讀
并不會(huì)對(duì)表中的任何記錄做加鎖操作,其他事務(wù)可以自由的對(duì)表中的記錄做改動(dòng)。
1.2 鎖定讀(Locking Reads)
1.2.1 共享鎖和獨(dú)占鎖
我們前邊說過,并發(fā)事務(wù)的讀-讀
情況并不會(huì)引起什么問題,不過對(duì)于寫-寫
、讀-寫
或寫-讀
這些情況可能會(huì)引起一些問題,需要使用MVCC
或者加鎖
的方式來解決它們。在使用加鎖
的方式解決問題時(shí),由于既要允許讀-讀
情況不受影響,又要使寫-寫
、讀-寫
或寫-讀
情況中的操作相互阻塞,所以MySQL給鎖分了個(gè)類:
-
共享鎖
,英文名:Shared Locks
,簡稱S鎖
。在事務(wù)要讀取一條記錄時(shí),需要先獲取該記錄的S鎖
。 -
獨(dú)占鎖
,也常稱排他鎖
,英文名:Exclusive Locks
,簡稱X鎖
。在事務(wù)要改動(dòng)一條記錄時(shí),需要先獲取該記錄的X鎖
。
假如事務(wù)T1
首先獲取了一條記錄的S鎖
之后,事務(wù)T2
接著也要訪問這條記錄:
-
如果事務(wù)
T2
想要再獲取一個(gè)記錄的S鎖
,那么事務(wù)T2
也會(huì)獲得該鎖,也就意味著事務(wù)T1
和T2
在該記錄上同時(shí)持有S鎖
。 -
如果事務(wù)
T2
想要再獲取一個(gè)記錄的X鎖
,那么此操作會(huì)被阻塞,直到事務(wù)T1
提交之后將S鎖
釋放掉。
如果事務(wù)T1
首先獲取了一條記錄的X鎖
之后,那么不管事務(wù)T2
接著想獲取該記錄的S鎖
還是X鎖
都會(huì)被阻塞,直到事務(wù)T1
提交。
所以我們說S鎖
和S鎖
是兼容的,S鎖
和X鎖
是不兼容的,X鎖
和X鎖
也是不兼容的,畫個(gè)表表示一下就是這樣:
兼容性 | X | S |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
1.2.2 鎖定讀的語句
我們前邊說在采用加鎖
方式解決臟讀、不可重復(fù)讀、幻讀這些問題時(shí),讀取一條記錄時(shí)需要獲取一下該記錄的S鎖
,其實(shí)這是不嚴(yán)謹(jǐn)?shù)模袝r(shí)候想在讀取記錄時(shí)就獲取記錄的X鎖
,來禁止別的事務(wù)讀寫該記錄,為此MySQL的提出了兩種比較特殊的SELECT
語句格式:
對(duì)讀取的記錄加S鎖:
SELECT ... LOCK IN SHARE MODE;
也就是在普通的SELECT
語句后邊加LOCK IN SHARE MODE
,如果當(dāng)前事務(wù)執(zhí)行了該語句,那么它會(huì)為讀取到的記錄加S鎖,這樣允許別的事務(wù)繼續(xù)獲取這些記錄的S鎖
(比方說別的事務(wù)也使用SELECT ... LOCK IN SHARE MODE
語句來讀取這些記錄),但是不能獲取這些記錄的X鎖
(比方說使用SELECT ... FOR UPDATE
語句來讀取這些記錄,或者直接修改這些記錄)。如果別的事務(wù)想要獲取這些記錄的X鎖
,那么它們會(huì)阻塞,直到當(dāng)前事務(wù)提交之后將這些記錄上的S鎖
釋放掉
對(duì)讀取的記錄加X鎖:
SELECT ... FOR UPDATE;
也就是在普通的SELECT
語句后邊加FOR UPDATE
,如果當(dāng)前事務(wù)執(zhí)行了該語句,那么它會(huì)為讀取到的記錄加X鎖
,這樣既不允許別的事務(wù)獲取這些記錄的S鎖
(比方說別的事務(wù)使用SELECT ... LOCK IN SHARE MODE
語句來讀取這些記錄),也不允許獲取這些記錄的X鎖
(比如說使用SELECT ... FOR UPDATE
語句來讀取這些記錄,或者直接修改這些記錄)。如果別的事務(wù)想要獲取這些記錄的S鎖
或者X鎖
,那么它們會(huì)阻塞,直到當(dāng)前事務(wù)提交之后將這些記錄上的X鎖
釋放掉
關(guān)于更多鎖定讀
的加鎖細(xì)節(jié)
我們稍后會(huì)詳細(xì)講解,稍安勿躁
1.3 寫操作
我們平常所用到的寫操作無非是DELETE
、UPDATE
、INSERT
這三種
-
DELETE:
對(duì)一條記錄做DELETE
操作的過程其實(shí)是先在B+樹
中定位到這條記錄的位置,然后獲取一下這條記錄的X鎖
,然后再執(zhí)行delete mark
操作。我們也可以把這個(gè)定位待刪除記錄在B+樹中位置的過程看成是一個(gè)獲取X鎖
的鎖定讀
。 -
UPDATE:
在對(duì)一條記錄做UPDATE操作時(shí)分為三種情況:-
如果未修改該記錄的鍵值并且被更新的列占用的存儲(chǔ)空間在修改前后未發(fā)生變化,則先在
B+
樹中定位到這條記錄的位置,然后再獲取一下記錄的X鎖
,最后在原記錄的位置進(jìn)行修改操作。其實(shí)我們也可以把這個(gè)定位待修改記錄在B+
樹中位置的過程看成是一個(gè)獲取X鎖
的鎖定讀
。 -
如果未修改該記錄的鍵值并且至少有一個(gè)被更新的列占用的存儲(chǔ)空間在修改前后發(fā)生變化,則先在
B+
樹中定位到這條記錄的位置,然后獲取一下記錄的X鎖
,將該記錄徹底刪除掉
(就是把記錄徹底移入垃圾鏈表),最后再插入一條新記錄。這個(gè)定位待修改記錄在B+
樹中位置的過程看成是一個(gè)獲取X鎖
的鎖定讀
,新插入的記錄由INSERT
操作提供的隱式鎖
進(jìn)行保護(hù)。 -
如果修改了該記錄的鍵值,則相當(dāng)于在原記錄上做
DELETE
操作之后再來一次INSERT
操作,加鎖操作就需要按照DELETE
和INSERT
的規(guī)則進(jìn)行了。
-
-
INSERT:
一般情況下,新插入一條記錄的操作并不加鎖,InnoDB
通過一種稱之為隱式鎖
來保護(hù)這條新插入的記錄在本事務(wù)提交前不被別的事務(wù)訪問,更多細(xì)節(jié)我們后邊看哈~小提示:
當(dāng)然,在一些特殊情況下INSERT操作也是會(huì)獲取鎖的,具體情況我們后邊學(xué)習(xí)
二、多粒度鎖
我們前邊提到的鎖
都是針對(duì)記錄的,也可以被稱之為行級(jí)鎖
或者行鎖
,對(duì)一條記錄加鎖影響的也只是這條記錄而已,我們就說這個(gè)鎖的粒度比較細(xì);其實(shí)一個(gè)事務(wù)也可以在表級(jí)別
進(jìn)行加鎖,自然就被稱之為表級(jí)鎖
或者表鎖
,對(duì)一個(gè)表加鎖影響整個(gè)表中的記錄,我們就說這個(gè)鎖的粒度比較粗。給表加的鎖也可以分為共享鎖
(S鎖
)和獨(dú)占鎖
(X鎖
)
-
給表加
S鎖
:如果一個(gè)事務(wù)給表加了S鎖,那么:-
別的事務(wù)可以繼續(xù)獲得該表的S鎖
-
別的事務(wù)可以繼續(xù)獲得該表中的某些記錄的S鎖
-
別的事務(wù)不可以繼續(xù)獲得該表的X鎖
-
別的事務(wù)不可以繼續(xù)獲得該表中的某些記錄的X鎖
-
-
給表加
X鎖
:如果一個(gè)事務(wù)給表加了X鎖(意味著該事務(wù)要獨(dú)占這個(gè)表),那么:-
別的事務(wù)不可以繼續(xù)獲得該表的S鎖
-
別的事務(wù)不可以繼續(xù)獲得該表中的某些記錄的S鎖
-
別的事務(wù)不可以繼續(xù)獲得該表的X鎖
-
別的事務(wù)不可以繼續(xù)獲得該表中的某些記錄的X鎖
-
上邊看著有點(diǎn)啰嗦,為了更好的理解這個(gè)表級(jí)別的S鎖
和X鎖
,我們舉一個(gè)現(xiàn)實(shí)生活中的例子。不知道各位同學(xué)都上過大學(xué)沒,我們以大學(xué)教學(xué)樓中的教室為例來分析一下加鎖的情況:
-
教室一般都是公用的,我們可以隨便選教室進(jìn)去上自習(xí)。當(dāng)然,教室不是自家的,一間教室可以容納很多同學(xué)同時(shí)上自習(xí),每當(dāng)一個(gè)人進(jìn)去上自習(xí),就相當(dāng)于在教室門口掛了一把
S鎖
,如果很多同學(xué)都進(jìn)去上自習(xí),相當(dāng)于教室門口掛了很多把S鎖(類似行級(jí)別的S鎖
)。 -
有的時(shí)候教室會(huì)進(jìn)行檢修,比方說換地板,換天花板,換燈管啥的,這些維修項(xiàng)目并不能同時(shí)開展。如果教室針對(duì)某個(gè)項(xiàng)目進(jìn)行檢修,就不允許別的同學(xué)來上自習(xí),也不允許其他維修項(xiàng)目進(jìn)行,此時(shí)相當(dāng)于教室門口會(huì)掛一把
X鎖
(類似行級(jí)別的X鎖
)。
上邊提到的這兩種鎖都是針對(duì)教室而言的,不過有時(shí)候我們會(huì)有一些特殊的需求:
-
有領(lǐng)導(dǎo)要來參觀教學(xué)樓的環(huán)境。
校領(lǐng)導(dǎo)考慮并不想影響同學(xué)們上自習(xí),但是此時(shí)不能有教室處于維修狀態(tài),所以可以在教學(xué)樓門口放置一把
S鎖
(類似表級(jí)別的S鎖
)。此時(shí):-
來上自習(xí)的學(xué)生們看到教學(xué)樓門口有
S鎖
,可以繼續(xù)進(jìn)入教學(xué)樓上自習(xí)。 -
修理工看到教學(xué)樓門口有
S鎖
,則先在教學(xué)樓門口等著,啥時(shí)候領(lǐng)導(dǎo)走了,把教學(xué)樓的S鎖
撤掉再進(jìn)入教學(xué)樓維修。
-
-
學(xué)校要占用教學(xué)樓進(jìn)行考試。
此時(shí)不允許教學(xué)樓中有正在上自習(xí)的教室,也不允許對(duì)教室進(jìn)行維修。所以可以在教學(xué)樓門口放置一把
X鎖
(類似表級(jí)別的X鎖
)。此時(shí):-
來上自習(xí)的學(xué)生們看到教學(xué)樓門口有
X鎖
,則需要在教學(xué)樓門口等著,啥時(shí)候考試結(jié)束,把教學(xué)樓的X鎖
撤掉再進(jìn)入教學(xué)樓上自習(xí)。 -
修理工看到教學(xué)樓門口有
X鎖
,則先在教學(xué)樓門口等著,啥時(shí)候考試結(jié)束,把教學(xué)樓的X鎖
撤掉再進(jìn)入教學(xué)樓維修。
-
但是這里頭有兩個(gè)問題:
-
如果我們想對(duì)教學(xué)樓整體上
S鎖
,首先需要確保教學(xué)樓中的沒有正在維修的教室,如果有正在維修的教室,需要等到維修結(jié)束才可以對(duì)教學(xué)樓整體上S鎖
。 -
如果我們想對(duì)教學(xué)樓整體上
X鎖
,首先需要確保教學(xué)樓中的沒有上自習(xí)的教室以及正在維修的教室,如果有上自習(xí)的教室或者正在維修的教室,需要等到全部上自習(xí)的同學(xué)都上完自習(xí)離開,以及維修工維修完教室離開后才可以對(duì)教學(xué)樓整體上X鎖
。
我們?cè)趯?duì)教學(xué)樓整體上鎖(表鎖
)時(shí),怎么知道教學(xué)樓中有沒有教室已經(jīng)被上鎖(行鎖
)了呢?依次檢查每一間教室門口有沒有上鎖?那這效率也太慢了吧!遍歷是不可能遍歷的,這輩子也不可能遍歷的,于是InnoDB
的提出了一種稱之為意向鎖
(英文名:Intention Locks
)的東東:
-
意向共享鎖
,英文名:Intention Shared Lock
,簡稱IS鎖
。當(dāng)事務(wù)準(zhǔn)備在某條記錄上加S鎖
時(shí),需要先在表級(jí)別加一個(gè)IS鎖
。 -
意向獨(dú)占鎖
,英文名:Intention Exclusive Lock
,簡稱IX鎖
。當(dāng)事務(wù)準(zhǔn)備在某條記錄上加X鎖
時(shí),需要先在表級(jí)別加一個(gè)IX鎖
。
視角回到教學(xué)樓和教室上來:
-
如果有學(xué)生到教室中上自習(xí),那么他先在整棟教學(xué)樓門口放一把
IS鎖
(表級(jí)鎖
),然后再到教室門口放一把S
鎖(行鎖
)。 -
如果有維修工到教室中維修,那么它先在整棟教學(xué)樓門口放一把
IX鎖
(表級(jí)鎖
),然后再到教室門口放一把X鎖
(行鎖
)。
之后:
-
如果有領(lǐng)導(dǎo)要參觀教學(xué)樓,也就是想在教學(xué)樓門口前放
S鎖
(表鎖
)時(shí),首先要看一下教學(xué)樓門口有沒有IX鎖
,如果有,意味著有教室在維修,需要等到維修結(jié)束把IX鎖
撤掉后才可以在整棟教學(xué)樓上加S鎖
。 -
如果有考試要占用教學(xué)樓,也就是想在教學(xué)樓門口前放
X鎖
(表鎖
)時(shí),首先要看一下教學(xué)樓門口有沒有IS鎖
或IX鎖
,如果有,意味著有教室在上自習(xí)或者維修,需要等到學(xué)生們上完自習(xí)以及維修結(jié)束把IS鎖
和IX鎖
撤掉后才可以在整棟教學(xué)樓上加X
鎖。
小提示:
學(xué)生在教學(xué)樓門口加IS鎖時(shí),是不關(guān)心教學(xué)樓門口是否有IX鎖的,維修工在教學(xué)樓門口加IX鎖時(shí),是不關(guān)心教學(xué)樓門口是否有IS鎖或者其他IX鎖的。IS和IX鎖只是為了判斷當(dāng)前時(shí)間教學(xué)樓里有沒有被占用的教室用的,也就是在對(duì)教學(xué)樓加S鎖或者X鎖時(shí)才會(huì)用到。
總結(jié)一下:IS
、IX鎖
是表級(jí)鎖
,它們的提出僅僅為了在之后加表級(jí)別的S鎖
和X鎖
時(shí)可以快速判斷表中的記錄是否被上鎖,以避免用遍歷的方式來查看表中有沒有上鎖的記錄,也就是說其實(shí)IS鎖
和IX鎖
是兼容的,IX鎖
和IX
鎖是兼容的。我們畫個(gè)表來看一下表級(jí)別的各種鎖的兼容性:
兼容性 | X | IX | S | IS |
---|---|---|---|---|
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
IX | 不兼容 | 兼容 | 不兼容 | 兼容 |
S | 不兼容 | 不兼容 | 兼容 | 兼容 |
IS | 不兼容 | 兼容 | 兼容 | 兼容 |
三、MySQL中的行鎖和表鎖
上邊說的都算是些理論知識(shí),其實(shí)MySQL
支持多種存儲(chǔ)引擎,不同存儲(chǔ)引擎對(duì)鎖的支持也是不一樣的。當(dāng)然,我們重點(diǎn)還是討論InnoDB
存儲(chǔ)引擎中的鎖,其他的存儲(chǔ)引擎只是稍微提一下~
3.1 其他存儲(chǔ)引擎中的鎖
對(duì)于MyISAM
、MEMORY
、MERGE
這些存儲(chǔ)引擎來說,它們只支持表級(jí)鎖
,而且這些引擎并不支持事務(wù)
,所以使用這些存儲(chǔ)引擎的鎖一般都是針對(duì)當(dāng)前會(huì)話來說的。比方說在Session 1
中對(duì)一個(gè)表執(zhí)行SELECT
操作,就相當(dāng)于為這個(gè)表加了一個(gè)表級(jí)別的S鎖
,如果在SELECT
操作未完成時(shí),Session 2
中對(duì)這個(gè)表執(zhí)行UPDATE
操作,相當(dāng)于要獲取表的X鎖
,此操作會(huì)被阻塞,直到Session 1
中的SELECT
操作完成,釋放掉表級(jí)別的S鎖
后,Session 2
中對(duì)這個(gè)表執(zhí)行UPDATE
操作才能繼續(xù)獲取X鎖
,然后執(zhí)行具體的更新語句
3.2 InnoDB存儲(chǔ)引擎中的鎖
InnoDB
存儲(chǔ)引擎既支持表鎖
,也支持行鎖
。表鎖
實(shí)現(xiàn)簡單,占用資源較少,不過粒度很粗,有時(shí)候你僅僅需要鎖住幾條記錄,但使用表鎖的話相當(dāng)于為表中的所有記錄都加鎖,所以性能比較差。行鎖
粒度更細(xì),可以實(shí)現(xiàn)更精準(zhǔn)的并發(fā)控制。下邊我們?cè)敿?xì)看一下
3.2.1 InnoDB中的表級(jí)鎖
表級(jí)別的S鎖、X鎖
在對(duì)某個(gè)表執(zhí)行SELECT
、INSERT
、DELETE
、UPDATE
語句時(shí),InnoDB
存儲(chǔ)引擎是不會(huì)為這個(gè)表添加表級(jí)別
的S鎖
或者X鎖
的。
另外,在對(duì)某個(gè)表執(zhí)行一些諸如ALTER TABLE
、DROP TABLE
這類的DDL
語句時(shí),其他事務(wù)對(duì)這個(gè)表并發(fā)執(zhí)行諸如SELECT
、INSERT
、DELETE
、UPDATE
的語句會(huì)發(fā)生阻塞,同理,某個(gè)事務(wù)中對(duì)某個(gè)表執(zhí)行SELECT
、INSERT
、DELETE
、UPDATE
語句時(shí),在其他會(huì)話中對(duì)這個(gè)表執(zhí)行DDL
語句也會(huì)發(fā)生阻塞。這個(gè)過程其實(shí)是通過在server
層使用一種稱之為元數(shù)據(jù)鎖
(英文名:Metadata Locks
,簡稱MDL
)來實(shí)現(xiàn)的,一般情況下也不會(huì)使用InnoDB
存儲(chǔ)引擎自己提供的表級(jí)別的S鎖
和X鎖
。
小提示:
在事務(wù)簡介的文章中我們說過,DDL語句執(zhí)行時(shí)會(huì)隱式的提交當(dāng)前會(huì)話中的事務(wù),這主要是DDL語句的執(zhí)行一般都會(huì)在若干個(gè)特殊事務(wù)中完成,在開啟這些特殊事務(wù)前,需要將當(dāng)前會(huì)話中的事務(wù)提交掉。
其實(shí)這個(gè)InnoDB
存儲(chǔ)引擎提供的表級(jí)S鎖
或者X鎖
是相當(dāng)雞肋,只會(huì)在一些特殊情況下,比方說崩潰恢復(fù)過程中用到。不過我們還是可以手動(dòng)獲取一下的,比方說在系統(tǒng)變量autocommit=0,innodb_table_locks = 1
時(shí),手動(dòng)獲取InnoDB
存儲(chǔ)引擎提供的表t的S鎖
或者X鎖
可以這么寫:
-
LOCK TABLES t READ:
InnoDB存儲(chǔ)引擎會(huì)對(duì)表t
加表級(jí)別的S鎖
。 -
LOCK TABLES t WRITE
:InnoDB存儲(chǔ)引擎會(huì)對(duì)表t
加表級(jí)別的X鎖
不過我們盡量避免在使用InnoDB
存儲(chǔ)引擎的表上使用LOCK TABLES
這樣的手動(dòng)鎖表語句,它們并不會(huì)提供什么額外的保護(hù),只是會(huì)降低并發(fā)能力而已。InnoDB
的厲害之處還是實(shí)現(xiàn)了更細(xì)粒度的行鎖,關(guān)于表級(jí)別的S鎖
和X鎖
大家了解一下就罷了。
表級(jí)別的IS鎖、IX鎖
當(dāng)我們?cè)趯?duì)使用InnoDB
存儲(chǔ)引擎的表的某些記錄加S鎖
之前,那就需要先在表級(jí)別加一個(gè)IS鎖
,當(dāng)我們?cè)趯?duì)使用InnoDB
存儲(chǔ)引擎的表的某些記錄加X鎖
之前,那就需要先在表級(jí)別加一個(gè)IX鎖
。IS鎖
和IX鎖
的使命只是為了后續(xù)在加表級(jí)別的S鎖
和X鎖
時(shí)判斷表中是否有已經(jīng)被加鎖的記錄,以避免用遍歷的方式來查看表中有沒有上鎖的記錄。更多關(guān)于IS鎖
和IX鎖
的解釋我們上邊已經(jīng)講解了,就不贅述了。
表級(jí)別的AUTO-INC鎖
在使用MySQL
過程中,我們可以為表的某個(gè)列添加AUTO_INCREMENT
屬性,之后在插入記錄時(shí),可以不指定該列的值,系統(tǒng)會(huì)自動(dòng)為它賦上遞增的值,比方說我們有一個(gè)表:
CREATE TABLE t (
id INT NOT NULL AUTO_INCREMENT,
c VARCHAR(100),
PRIMARY KEY (id)
);
由于這個(gè)表的id字段聲明了AUTO_INCREMENT
,也就意味著在書寫插入語句時(shí)不需要為其賦值,比方說這樣:
INSERT INTO t(c) VALUES('aa'), ('bb');
上邊的插入語句并沒有為id
列顯式賦值,所以系統(tǒng)會(huì)自動(dòng)為它賦上遞增的值,效果就是這樣:
mysql> SELECT * FROM t;
+----+------+
| id | c |
+----+------+
| 1 | aa |
| 2 | bb |
+----+------+
2 rows in set (0.00 sec)
系統(tǒng)實(shí)現(xiàn)這種自動(dòng)給AUTO_INCREMENT
修飾的列遞增賦值的原理主要是兩個(gè):
-
采用
AUTO-INC
鎖,也就是在執(zhí)行插入語句時(shí)就在表級(jí)別加一個(gè)AUTO-INC
鎖,然后為每條待插入記錄的AUTO_INCREMENT
修飾的列分配遞增的值,在該語句執(zhí)行結(jié)束后,再把AUTO-INC
鎖釋放掉。這樣一個(gè)事務(wù)在持有AUTO-INC
鎖的過程中,其他事務(wù)的插入語句都要被阻塞,可以保證一個(gè)語句中分配的遞增值是連續(xù)的。如果我們的插入語句在執(zhí)行前不可以確定具體要插入多少條記錄(無法預(yù)計(jì)即將插入記錄的數(shù)量),比方說使用
INSERT ... SELECT、REPLACE ... SELECT
或者LOAD DATA
這種插入語句,一般是使用AUTO-INC
鎖為AUTO_INCREMENT
修飾的列生成對(duì)應(yīng)的值。小提示:
需要注意一下的是,這個(gè)AUTO-INC鎖的作用范圍只是單個(gè)插入語句,插入語句執(zhí)行完成后,這個(gè)鎖就被釋放了,跟我們之前介紹的鎖在事務(wù)結(jié)束時(shí)釋放是不一樣的 -
采用一個(gè)輕量級(jí)的鎖,在為插入語句生成
AUTO_INCREMENT
修飾的列的值時(shí)獲取一下這個(gè)輕量級(jí)鎖,然后生成本次插入語句需要用到的AUTO_INCREMENT
列的值之后,就把該輕量級(jí)鎖釋放掉,并不需要等到整個(gè)插入語句執(zhí)行完才釋放鎖。如果我們的插入語句在執(zhí)行前就可以確定具體要插入多少條記錄,比方說我們上邊舉的關(guān)于表t的例子中,在語句執(zhí)行前就可以確定要插入2條記錄,那么一般采用輕量級(jí)鎖的方式對(duì)
AUTO_INCREMENT
修飾的列進(jìn)行賦值。這種方式可以避免鎖定表,可以提升插入性能。小提示:
InnoDB提供了一個(gè)稱之為innodb_autoinc_lock_mode
的系統(tǒng)變量來控制到底使用上述兩種方式中的哪種來為AUTO_INCREMENT
修飾的列進(jìn)行賦值
當(dāng)innodb_autoinc_lock_mode
值為0
時(shí),一律采用AUTO-INC鎖
;
當(dāng)innodb_autoinc_lock_mode
值為2
時(shí),一律采用輕量級(jí)鎖
;
當(dāng)innodb_autoinc_lock_mode
值為1
時(shí),兩種方式混著來
(也就是在插入記錄數(shù)量確定時(shí)采用輕量級(jí)鎖,不確定時(shí)使用AUTO-INC鎖)。
不過當(dāng)innodb_autoinc_lock_mode
值為2
時(shí),可能會(huì)造成不同事務(wù)中的插入語句為AUTO_INCREMENT
修飾的列生成的值是交叉的,在有主從復(fù)制的場景中是不安全的。
3.2.2 InnoDB中的行級(jí)鎖
很遺憾的通知大家一個(gè)不好的消息,上邊講的都是鋪墊,本章真正的重點(diǎn)才剛剛開始
行鎖
,也稱為記錄鎖
,顧名思義就是在記錄上加的鎖。不過InnoDB
把一個(gè)行鎖玩出了各種花樣,也就是把行鎖分成了各種類型。換句話說即使對(duì)同一條記錄加行鎖,如果類型不同,起到的功效也是不同的。為了學(xué)習(xí)的順利發(fā)展,我們還是先將之前學(xué)習(xí)的MVCC
時(shí)用到的表抄一遍:
mysql> CREATE TABLE hero(
number INT PRIMARY KEY,
name VARCHAR(4),
country VARCHAR(2)
);
Query OK, 0 rows affected (0.03 sec)
我們主要是想用這個(gè)表存儲(chǔ)王者的英雄,然后向這個(gè)表里插入幾條記錄:
mysql> INSERT INTO hero VALUES
(1, 'l劉備', '蜀國'),
(3, 'z諸葛亮', '蜀國'),
(8, 'c曹操', '蜀國'),
(15, 'x項(xiàng)羽', '西楚'),
(20, 's孫權(quán)', '吳國');
Query OK, 5 rows affected (0.01 sec)
Records: 5 Duplicates: 0 Warnings: 0
小提示:
為啥要在’劉備’、‘曹操’、‘孫權(quán)’前邊加上’l’、‘c’、‘s’這幾個(gè)字母呀?這個(gè)主要是因?yàn)槲覀儾捎胾tf8mb4字符集,該字符集并沒有對(duì)應(yīng)的按照漢語拼音進(jìn)行排序的比較規(guī)則,也就是說’劉備’、‘曹操’、'孫權(quán)’這幾個(gè)字符串的排序并不是按照它們漢語拼音進(jìn)行排序的,所以在漢字前邊加上了漢字對(duì)應(yīng)的拼音的第一個(gè)字母,這樣在排序時(shí)就是按照漢語拼音進(jìn)行排序。
另外,我們故意把各條記錄number
列的值搞得很分散,后邊會(huì)用到,稍安勿躁哈~我們把hero
表中的聚簇索引的示意圖畫一下:
當(dāng)然,我們把B+樹
的索引結(jié)構(gòu)做了一個(gè)超級(jí)簡化,只把索引中的記錄給拿了出來,我們這里只是想強(qiáng)調(diào)聚簇索引中的記錄是按照主鍵大小排序的,并且省略掉了聚簇索引中的隱藏列,大家心里明白就好(不理解索引結(jié)構(gòu)的同學(xué)可以去前邊的文章中查看)。
現(xiàn)在準(zhǔn)備工作做完了,下邊我們來看看都有哪些常用的行鎖類型。
-
Record Locks:
我們前邊提到的記錄鎖
就是這種類型,也就是僅僅把一條記錄鎖上,我決定給這種類型的鎖起一個(gè)比較不正經(jīng)的名字:正經(jīng)記錄鎖
(請(qǐng)?jiān)试S我皮一下,我實(shí)在不知道該叫個(gè)啥名好)。官方的類型名稱為:LOCK_REC_NOT_GAP
。比方說我們把number
值為8
的那條記錄加一個(gè)正經(jīng)記錄鎖的示意圖如下:
正經(jīng)記錄鎖是有S鎖
和X鎖
之分的,讓我們分別稱之為S型正經(jīng)記錄鎖
和X型正經(jīng)記錄鎖
吧,當(dāng)一個(gè)事務(wù)獲取了一條記錄的S型正經(jīng)記錄鎖
后,其他事務(wù)也可以繼續(xù)獲取該記錄的S型正經(jīng)記錄鎖
,但不可以繼續(xù)獲取X型正經(jīng)記錄
鎖;當(dāng)一個(gè)事務(wù)獲取了一條記錄的X型正經(jīng)記錄鎖
后,其他事務(wù)既不可以繼續(xù)獲取該記錄的S型正經(jīng)記錄鎖
,也不可以繼續(xù)獲取X型正經(jīng)記錄鎖
; -
Gap Locks:
我們說MySQL
在REPEATABLE READ
隔離級(jí)別下是可以解決幻讀問題的,解決方案有兩種,可以使用MVCC
方案解決,也可以采用加鎖
方案解決。但是在使用加鎖方案解決時(shí)有個(gè)大問題,就是事務(wù)在第一次執(zhí)行讀取操作時(shí),那些幻影記錄尚不存在,我們無法給這些幻影記錄加上正經(jīng)記錄鎖。不過這難不倒InnoDB
,他們提出了一種稱之為Gap Locks
的鎖,官方的類型名稱為:LOCK_GAP
,我們也可以簡稱為gap鎖
。比方說我們把number
值為8
的那條記錄加一個(gè)gap
鎖的示意圖如下:
如圖中為number
值為8
的記錄加了gap鎖
,意味著不允許別的事務(wù)在number
值為8
的記錄前邊的間隙插入新記錄,其實(shí)就是number
列的值(3, 8)
這個(gè)區(qū)間的新記錄是不允許立即插入的。比方說有另外一個(gè)事務(wù)再想插入一條number
值為4
的新記錄,它定位到該條新記錄的下一條記錄的number
值為8
,而這條記錄上又有一個(gè)gap鎖
,所以就會(huì)阻塞插入操作,直到擁有這個(gè)gap鎖
的事務(wù)提交了之后,number
列的值在區(qū)間(3, 8)
中的新記錄才可以被插入。這個(gè)
gap鎖
的提出僅僅是為了防止插入幻影記錄而提出的,雖然有共享gap鎖
和獨(dú)占gap鎖
這樣的說法,但是它們起到的作用都是相同的。而且如果你對(duì)一條記錄加了gap鎖
(不論是共享gap鎖
還是獨(dú)占gap鎖
),并不會(huì)限制其他事務(wù)對(duì)這條記錄加正經(jīng)記錄鎖
或者繼續(xù)加gap鎖
不知道大家發(fā)現(xiàn)了一個(gè)問題沒,給一條記錄加了
gap鎖
只是不允許其他事務(wù)往這條記錄前邊的間隙插入新記錄,那對(duì)于最后一條記錄之后的間隙,也就是hero
表中number
值為20
的記錄之后的間隙該咋辦呢?也就是說給哪條記錄加gap鎖
才能阻止其他事務(wù)插入number
值在(20, +∞)
這個(gè)區(qū)間的新記錄呢?這時(shí)候應(yīng)該想起我們?cè)谇斑厙Z叨數(shù)據(jù)頁時(shí)介紹的兩條偽記錄了-
Infimum
記錄,表示該頁面中最小的記錄。 -
Supremum
記錄,表示該頁面中最大的記錄。
為了實(shí)現(xiàn)阻止其他事務(wù)插入
number
值在(20, +∞)
這個(gè)區(qū)間的新記錄,我們可以給索引中的最后一條記錄,也就是number
值為20
的那條記錄所在頁面的Supremum
記錄加上一個(gè)gap鎖
,畫個(gè)圖就是這樣:
這樣就可以阻止其他事務(wù)插入number值在(20, +∞)
這個(gè)區(qū)間的新記錄。為了大家理解方便,之后的索引示意圖中都會(huì)把這個(gè)Supremum
記錄畫出來 -
-
Next-Key Locks:
有時(shí)候我們既想鎖住某條記錄,又想阻止其他事務(wù)在該記錄前邊的間隙插入新記錄,所以InnoDB
就提出了一種稱之為Next-Key Locks
的鎖,官方的類型名稱為:LOCK_ORDINARY
,我們也可以簡稱為next-key
鎖。比方說我們把number
值為8
的那條記錄加一個(gè)next-key
鎖的示意圖如下:next-key鎖
的本質(zhì)就是一個(gè)正經(jīng)記錄鎖
和一個(gè)gap鎖
的合體,它既能保護(hù)該條記錄,又能阻止別的事務(wù)將新記錄插入被保護(hù)記錄前邊的間隙。 -
Insert Intention Locks:
我們說一個(gè)事務(wù)在插入一條記錄時(shí)需要判斷一下插入位置是不是被別的事務(wù)加了所謂的gap鎖
(next-key鎖
也包含gap鎖,后邊就不強(qiáng)調(diào)了),如果有的話,插入操作需要等待,直到擁有gap鎖
的那個(gè)事務(wù)提交。但是InnoDB
規(guī)定事務(wù)在等待的時(shí)候也需要在內(nèi)存中生成一個(gè)鎖結(jié)構(gòu),表明有事務(wù)想在某個(gè)間隙中插入新記錄,但是現(xiàn)在在等待。InnoDB
把這種類型的鎖命名為Insert Intention Locks
,官方的類型名稱為:LOCK_INSERT_INTENTION
,我們也可以稱為插入意向鎖
。比方說我們把
number
值為8
的那條記錄加一個(gè)插入意向鎖的示意圖如下:
為了讓大家徹底理解這個(gè)插入意向鎖
的功能,我們還是舉個(gè)例子然后畫個(gè)圖表示一下。比方說現(xiàn)在T1
為number
值為8
的記錄加了一個(gè)gap鎖
,然后T2
和T3
分別想向hero
表中插入number
值分別為4
、5
的兩條記錄,所以現(xiàn)在為number
值為8
的記錄加的鎖的示意圖就如下所示:小提示:
我們?cè)阪i結(jié)構(gòu)中又新添了一個(gè)type屬性,表明該鎖的類型。稍后會(huì)全面介紹InnoDB存儲(chǔ)引擎中的一個(gè)鎖結(jié)構(gòu)到底長什么樣從圖中可以看到,由于
T1
持有gap鎖
,所以T2
和T3
需要生成一個(gè)插入意向鎖
的鎖結(jié)構(gòu)
并且處于等待狀態(tài)
。當(dāng)T1
提交后會(huì)把它獲取到的鎖都釋放掉,這樣T2
和T3
就能獲取到對(duì)應(yīng)的插入意向鎖了(本質(zhì)上就是把插入意向鎖對(duì)應(yīng)鎖結(jié)構(gòu)的is_waiting屬性改為false
),T2
和T3
之間也并不會(huì)相互阻塞,它們可以同時(shí)獲取到number
值為8
的插入意向鎖
,然后執(zhí)行插入操作。事實(shí)上插入意向鎖并不會(huì)阻止別的事務(wù)繼續(xù)獲取該記錄上任何類型的鎖
(插入意向鎖
就是這么雞肋)。 -
隱式鎖:
我們前邊說一個(gè)事務(wù)在執(zhí)行INSERT
操作時(shí),如果即將插入的間隙已經(jīng)被其他事務(wù)加了gap鎖
,那么本次INSERT
操作會(huì)阻塞,并且當(dāng)前事務(wù)會(huì)在該間隙上加一個(gè)插入意向鎖
,否則一般情況下INSERT
操作是不加鎖的。那如果一個(gè)事務(wù)首先插入了一條記錄(此時(shí)并沒有與該記錄關(guān)聯(lián)的鎖結(jié)構(gòu)),然后另一個(gè)事務(wù):-
立即使用
SELECT ... LOCK IN SHARE MODE
語句讀取這條記錄,也就是在要獲取這條記錄的S鎖
,或者使用SELECT ... FOR UPDATE
語句讀取這條記錄,也就是要獲取這條記錄的X鎖
,該咋辦?如果允許這種情況的發(fā)生,那么可能產(chǎn)生臟讀問題。
-
立即修改這條記錄,也就是要獲取這條記錄的
X鎖
,該咋辦?如果允許這種情況的發(fā)生,那么可能產(chǎn)生臟寫問題。
這時(shí)候我們前邊嘮叨了很多遍的
事務(wù)id
又要起作用了。我們把聚簇索引
和二級(jí)索引
中的記錄分開看一下:-
情景一:
對(duì)于聚簇索引記錄來說,有一個(gè)trx_id
隱藏列,該隱藏列記錄著最后改動(dòng)該記錄的事務(wù)id
。那么如果在當(dāng)前事務(wù)中新插入一條聚簇索引記錄后,該記錄的trx_id
隱藏列代表的的就是當(dāng)前事務(wù)的事務(wù)id
,如果其他事務(wù)此時(shí)想對(duì)該記錄添加S鎖
或者X鎖
時(shí),首先會(huì)看一下該記錄的trx_id
隱藏列代表的事務(wù)是否是當(dāng)前的活躍事務(wù),如果是的話,那么就幫助當(dāng)前事務(wù)創(chuàng)建一個(gè)X鎖
(也就是為當(dāng)前事務(wù)創(chuàng)建一個(gè)鎖結(jié)構(gòu),is_waiting屬性是false
),然后自己進(jìn)入等待狀態(tài)(也就是為自己也創(chuàng)建一個(gè)鎖結(jié)構(gòu)
,is_waiting屬性是true
)。 -
情景二:
對(duì)于二級(jí)索引記錄來說,本身并沒有trx_id
隱藏列,但是在二級(jí)索引頁面的Page Header
部分有一個(gè)PAGE_MAX_TRX_ID
屬性,該屬性代表對(duì)該頁面做改動(dòng)的最大的事務(wù)id
,如果PAGE_MAX_TRX_ID
屬性值小于當(dāng)前最小的活躍事務(wù)id
,那么說明對(duì)該頁面做修改的事務(wù)都已經(jīng)提交了,否則就需要在頁面中定位到對(duì)應(yīng)的二級(jí)索引記錄,然后回表找到它對(duì)應(yīng)的聚簇索引記錄,然后再重復(fù)情景一
的做法。
通過上邊的敘述我們知道,一個(gè)事務(wù)對(duì)新插入的記錄可以
不顯式的加鎖(生成一個(gè)鎖結(jié)構(gòu))
,但是由于事務(wù)id
的存在,相當(dāng)于加了一個(gè)隱式鎖
。別的事務(wù)在對(duì)這條記錄加S鎖
或者X鎖
時(shí),由于隱式鎖
的存在,會(huì)先幫助當(dāng)前事務(wù)生成一個(gè)鎖結(jié)構(gòu)
,然后自己再生成一個(gè)鎖結(jié)構(gòu)
后進(jìn)入等待狀態(tài)小提示:
除了插入意向鎖,在一些特殊情況下INSERT還會(huì)獲取一些鎖,我們稍后學(xué)習(xí)~ -
四、InnoDB鎖的內(nèi)存結(jié)構(gòu)
我們前邊說對(duì)一條記錄加鎖的本質(zhì)就是在內(nèi)存中創(chuàng)建一個(gè)鎖結(jié)構(gòu)與之關(guān)聯(lián),那么是不是一個(gè)事務(wù)對(duì)多條記錄加鎖,就要?jiǎng)?chuàng)建多個(gè)鎖結(jié)構(gòu)呢?比方說事務(wù)T1要執(zhí)行下邊這個(gè)語句:
# 事務(wù)T1
SELECT * FROM hero LOCK IN SHARE MODE;
很顯然這條語句需要為hero
表中的所有記錄進(jìn)行加鎖,那是不是需要為每條記錄都生成一個(gè)鎖結(jié)構(gòu)
呢?其實(shí)理論上創(chuàng)建多個(gè)鎖結(jié)構(gòu)沒問題,反而更容易理解,但是誰知道你在一個(gè)事務(wù)里想對(duì)多少記錄加鎖呢,如果一個(gè)事務(wù)要獲取10000條記錄的鎖,要生成10000個(gè)這樣的結(jié)構(gòu)也太虧了吧!InnoDB
的決定在對(duì)不同記錄加鎖時(shí),如果符合下邊這些條件:
-
在同一個(gè)事務(wù)中進(jìn)行加鎖操作
-
被加鎖的記錄在同一個(gè)頁面中
-
加鎖的類型是一樣的
-
等待狀態(tài)是一樣的
那么這些記錄的鎖就可以被放到一個(gè)鎖結(jié)構(gòu)中。當(dāng)然,這么空口白牙的說有點(diǎn)兒抽象,我們還是畫個(gè)圖來看看InnoDB
存儲(chǔ)引擎中的鎖結(jié)構(gòu)
具體長啥樣吧:
我們看看這個(gè)結(jié)構(gòu)里邊的各種信息都是干嘛的:
-
鎖所在的事務(wù)信息:
不論是表鎖還是行鎖,都是在事務(wù)執(zhí)行過程中生成的,哪個(gè)事務(wù)生成了這個(gè)鎖結(jié)構(gòu),這里就記載著這個(gè)事務(wù)的信息。小提示:
實(shí)際上這個(gè)所謂的鎖所在的事務(wù)信息在內(nèi)存結(jié)構(gòu)中只是一個(gè)指針而已,所以不會(huì)占用多大內(nèi)存空間,通過指針可以找到內(nèi)存中關(guān)于該事務(wù)的更多信息,比方說事務(wù)id是什么。下邊介紹的所謂的索引信息其實(shí)也是一個(gè)指針 -
索引信息:
對(duì)于行鎖來說,需要記錄一下加鎖的記錄是屬于哪個(gè)索引的。 -
表鎖/行鎖信息:
表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)在這個(gè)位置的內(nèi)容是不同的-
表鎖:
記載著這是對(duì)哪個(gè)表加的鎖,還有其他的一些信息。 -
行鎖:
記載了三個(gè)重要的信息-
Space ID:
記錄所在表空間。 -
Page Number:
記錄所在頁號(hào)。 -
n_bits:
對(duì)于行鎖來說,一條記錄就對(duì)應(yīng)著一個(gè)比特位,一個(gè)頁面中包含很多記錄,用不同的比特位來區(qū)分到底是哪一條記錄加了鎖。為此在行鎖結(jié)構(gòu)的末尾放置了一堆比特位,這個(gè)n_bits屬性代表使用了多少比特位小提示:
并不是該頁面中有多少記錄,n_bits屬性的值就是多少。為了讓之后在頁面中插入了新記錄后也不至于重新分配鎖結(jié)構(gòu),所以n_bits的值一般都比頁面中記錄條數(shù)多一些
-
-
-
type_mode:
這是一個(gè)32位的數(shù),被分成了lock_mode
、lock_type
和rec_lock_type
三個(gè)部分,如圖所示-
鎖的模式(
lock_mode
),占用低4位,可選的值如下:-
LOCK_IS
(十進(jìn)制的0
):表示共享意向鎖,也就是IS鎖
。 -
LOCK_IX
(十進(jìn)制的1
):表示獨(dú)占意向鎖,也就是IX鎖
。 -
LOCK_S
(十進(jìn)制的2
):表示共享鎖,也就是S鎖
。 -
LOCK_X
(十進(jìn)制的3
):表示獨(dú)占鎖,也就是X鎖
。 -
LOCK_AUTO_INC
(十進(jìn)制的4
):表示AUTO-INC鎖
。小提示:
在InnoDB存儲(chǔ)引擎中,LOCK_IS,LOCK_IX,LOCK_AUTO_INC都算是表級(jí)鎖的模式,LOCK_S和LOCK_X既可以算是表級(jí)鎖的模式,也可以是行級(jí)鎖的模式
-
-
鎖的類型(
lock_type
),占用第5~8位,不過現(xiàn)階段只有第5位和第6位被使用:-
LOCK_TABLE
(十進(jìn)制的16
),也就是當(dāng)?shù)?個(gè)比特位置為1時(shí),表示表級(jí)鎖。 -
LOCK_REC
(十進(jìn)制的32
),也就是當(dāng)?shù)?個(gè)比特位置為1時(shí),表示行級(jí)鎖
-
-
行鎖的具體類型(
rec_lock_type
),使用其余的位來表示。只有在lock_type
的值為LOCK_REC
時(shí),也就是只有在該鎖為行級(jí)鎖時(shí),才會(huì)被細(xì)分為更多的類型:-
LOCK_ORDINARY
(十進(jìn)制的0
):表示next-key鎖
。 -
LOCK_GAP
(十進(jìn)制的512
):也就是當(dāng)?shù)?0個(gè)比特位置為1時(shí),表示gap鎖
。 -
LOCK_REC_NOT_GAP
(十進(jìn)制的1024
):也就是當(dāng)?shù)?1個(gè)比特位置為1時(shí),表示正經(jīng)記錄鎖
。 -
LOCK_INSERT_INTENTION
(十進(jìn)制的2048
):也就是當(dāng)?shù)?2個(gè)比特位置為1時(shí),表示插入意向鎖
。 -
其他的類型:還有一些不常用的類型我們就不多說了。
怎么還沒看見
is_waiting
屬性呢?這主要還是InnoDB把is_waiting
屬性也放到了type_mode
這個(gè)32位的數(shù)字中:-
LOCK_WAIT
(十進(jìn)制的256
) :也就是當(dāng)?shù)?個(gè)比特位置為1時(shí),表示is_waiting
為true
,也就是當(dāng)前事務(wù)尚未獲取到鎖,處在等待狀態(tài);當(dāng)這個(gè)比特位為0
時(shí),表示is_waiting
為false
,也就是當(dāng)前事務(wù)獲取鎖成功。
-
-
-
其他信息:為了更好的管理系統(tǒng)運(yùn)行過程中生成的各種鎖結(jié)構(gòu)而設(shè)計(jì)了各種哈希表和鏈表,為了簡化討論,我們忽略這部分信息哈~
-
一堆比特位:
如果是行鎖結(jié)構(gòu)
的話,在該結(jié)構(gòu)末尾還放置了一堆比特位,比特位的數(shù)量是由上邊提到的n_bits
屬性表示的。我們前邊學(xué)習(xí)InnoDB
記錄結(jié)構(gòu)的時(shí)候說過,頁面中的每條記錄在記錄頭信息中都包含一個(gè)heap_no
屬性,偽記錄Infimum
的heap_no
值為0
,Supremum
的heap_no
值為1
,之后每插入一條記錄,heap_no
值就增1
。鎖結(jié)構(gòu)最后的一堆比特位就對(duì)應(yīng)著一個(gè)頁面中的記錄,一個(gè)比特位映射一個(gè)heap_no
,不過為了編碼方便,映射方式有點(diǎn)怪:小提示:
這么怪的映射方式純粹是為了敲代碼方便,大家不要大驚小怪,只需要知道一個(gè)比特位映射到頁內(nèi)的一條記錄就好了
可能上邊的描述大家覺得還是有些抽象,我們還是舉個(gè)例子說明一下。比方說現(xiàn)在有兩個(gè)事務(wù)T1
和T2
想對(duì)hero
表中的記錄進(jìn)行加鎖,hero
表中記錄比較少,假設(shè)這些記錄都存儲(chǔ)在所在的表空間號(hào)為67
,頁號(hào)為3
的頁面上,那么如果:
-
T1
想對(duì)number
值為15
的這條記錄加S型正常記錄鎖
,在對(duì)記錄加行鎖之前,需要先加表級(jí)別的IS
鎖,也就是會(huì)生成一個(gè)表級(jí)鎖的內(nèi)存結(jié)構(gòu),不過我們這里不關(guān)心表級(jí)鎖,所以就忽略掉了哈~ 接下來分析一下生成行鎖結(jié)構(gòu)的過程:-
事務(wù)
T1
要進(jìn)行加鎖,所以鎖結(jié)構(gòu)的鎖所在事務(wù)信息
指的就是T1
。 -
直接對(duì)聚簇索引進(jìn)行加鎖,所以索引信息指的其實(shí)就是
PRIMARY
索引。 -
由于是行鎖,所以接下來需要記錄的是三個(gè)重要信息:
-
Space ID
:表空間號(hào)為67
。 -
Page Number
:頁號(hào)為3
。 -
n_bits
:我們的hero
表中現(xiàn)在只插入了5條用戶記錄,但是在初始分配比特位時(shí)會(huì)多分配一些,這主要是為了在之后新增記錄時(shí)不用頻繁分配比特位。其實(shí)計(jì)算n_bits
有一個(gè)公式:n_bits = (1 + ((n_recs + LOCK_PAGE_BITMAP_MARGIN) / 8)) * 8
其中
n_recs
指的是當(dāng)前頁面中一共有多少條記錄(算上偽記錄和在垃圾鏈表中的記錄),比方說現(xiàn)在hero
表一共有7
條記錄(5條用戶記錄和2條偽記錄),所以n_recs
的值就是7
,LOCK_PAGE_BITMAP_MARGIN
是一個(gè)固定的值64,所以本次加鎖的n_bits
值就是:n_bits = (1 + ((7 + 64) / 8)) * 8 = 72
-
type_mode
是由三部分組成的:-
lock_mode
,這是對(duì)記錄加S鎖
,它的值為LOCK_S
。 -
lock_type
,這是對(duì)記錄進(jìn)行加鎖,也就是行鎖,所以它的值為LOCK_REC
。 -
rec_lock_type
,這是對(duì)記錄加正經(jīng)記錄鎖
,也就是類型為LOCK_REC_NOT_GAP
的鎖。另外,由于當(dāng)前沒有其他事務(wù)對(duì)該記錄加鎖,所以應(yīng)當(dāng)獲取到鎖,也就是LOCK_WAIT
代表的二進(jìn)制位應(yīng)該是0。
-
綜上所屬,此次加鎖的
type_mode
的值應(yīng)該是:type_mode = LOCK_S | LOCK_REC | LOCK_REC_NOT_GAP
也就是:
type_mode = 2 | 32 | 1024 = 1058
-
-
其他信息
:略~ -
一堆比特位
:因?yàn)?code>number值為15
的記錄heap_no
值為5
,根據(jù)上邊列舉的比特位和heap_no
的映射圖來看,應(yīng)該是第一個(gè)字節(jié)從低位往高位數(shù)第6個(gè)比特位被置為1,就像這樣:
-
綜上所述,事務(wù)T1
為number
值為5
的記錄加鎖生成的鎖結(jié)構(gòu)就如下圖所示:
-
T2
想對(duì)number
值為3
、8
、15
的這三條記錄加X
型的next-key
鎖,在對(duì)記錄加行鎖之前,需要先加表級(jí)別的IX鎖
,也就是會(huì)生成一個(gè)表級(jí)鎖的內(nèi)存結(jié)構(gòu)
,不過我們這里不關(guān)心表級(jí)鎖,所以就忽略掉了哈~現(xiàn)在
T2
要為3
條記錄加鎖,number
為3
、8
的兩條記錄由于沒有其他事務(wù)加鎖,所以可以成功獲取這條記錄的X型next-key鎖
,也就是生成的鎖結(jié)構(gòu)的is_waiting
屬性為false
;但是number
為15
的記錄已經(jīng)被T1
加了S型正經(jīng)記錄鎖
,T2
是不能獲取到該記錄的X型next-key鎖
的,也就是生成的鎖結(jié)構(gòu)的is_waiting
屬性為true
。因?yàn)榈却隣顟B(tài)不相同,所以這時(shí)候會(huì)生成兩個(gè)鎖結(jié)構(gòu)
。這兩個(gè)鎖結(jié)構(gòu)中相同的屬性如下:-
事務(wù)
T2
要進(jìn)行加鎖,所以鎖結(jié)構(gòu)的鎖所在事務(wù)
信息指的就是T2
。 -
直接對(duì)聚簇索引進(jìn)行加鎖,所以索引信息指的其實(shí)就是
PRIMARY
索引。 -
由于是行鎖,所以接下來需要記錄是三個(gè)重要信息:
-
Space ID
:表空間號(hào)為67。 -
Page Number
:頁號(hào)為3。 -
n_bits
:此屬性生成策略同T1中一樣,該屬性的值為72 -
type_mode
是由三部分組成的:-
lock_mode
,這是對(duì)記錄加X鎖
,它的值為LOCK_X
。 -
lock_type
,這是對(duì)記錄進(jìn)行加鎖,也就是行鎖,所以它的值為LOCK_REC
。 -
rec_lock_type
,這是對(duì)記錄加next-key
鎖,也就是類型為LOCK_ORDINARY
的鎖
-
-
-
其他信息
:略~
不同的屬性如下:
-
為
number
為3
、8
的記錄生成的鎖結(jié)構(gòu):-
type_mode
值:由于可以獲取到鎖,所以is_waiting
屬性為false
,也就是LOCK_WAIT
代表的二進(jìn)制位被置0
。所以:type_mode = LOCK_X | LOCK_REC |LOCK_ORDINARY
也就是
type_mode = 3 | 32 | 0 = 35
-
一堆比特位
:因?yàn)?code>number值為3
、8
的記錄heap_no
值分別為3
、4
,根據(jù)上邊列舉的比特位和heap_no
的映射圖來看,應(yīng)該是第一個(gè)字節(jié)從低位往高位數(shù)第4
、5
個(gè)比特位被置為1
,就像這樣:
綜上所述,事務(wù)T2為number值為3、8兩條記錄加鎖生成的鎖結(jié)構(gòu)就如下圖所示:
-
-
為number為15的記錄生成的鎖結(jié)構(gòu):
-
type_mode值:
由于不可以獲取到鎖,所以is_waiting
屬性為true
,也就是LOCK_WAIT
代表的二進(jìn)制位被置1。所以:type_mode = LOCK_X | LOCK_REC |LOCK_ORDINARY | LOCK_WAIT
也就是
type_mode = 3 | 32 | 0 | 256 = 291
-
一堆比特位:
因?yàn)?code>number值為15
的記錄heap_no
值為5
,根據(jù)上邊列舉的比特位和heap_no
的映射圖來看,應(yīng)該是第一個(gè)字節(jié)從低位往高位數(shù)第6個(gè)比特位被置為1,就像這樣:
綜上所述,事務(wù)
T2
為number
值為15
的記錄加鎖生成的鎖結(jié)構(gòu)就如下圖所示: -
綜上所述,事務(wù)
T1
先獲取number
值為15
的S型正經(jīng)記錄鎖
,然后事務(wù)T2
獲取number
值為3
、8
、15
的X型正經(jīng)記錄鎖
共需要生成3
個(gè)鎖結(jié)構(gòu)
小提示:
上邊事務(wù)T2在對(duì)number值分別為3、8、15這三條記錄加鎖的情景中,是按照先對(duì)number值為3的記錄加鎖、再對(duì)number值為8的記錄加鎖,最后對(duì)number值為15的記錄加鎖的順序進(jìn)行的,如果我們一開始就對(duì)number值為15的記錄加鎖,那么該事務(wù)在為number值為15的記錄生成一個(gè)鎖結(jié)構(gòu)后,直接就進(jìn)入等待狀態(tài),就不為number值為3、8的兩條記錄生成鎖結(jié)構(gòu)了。在事務(wù)T1提交后會(huì)把在number值為15的記錄上獲取的鎖釋放掉,然后事務(wù)T2就可以獲取該記錄上的鎖,這時(shí)再對(duì)number值為3、8的兩條記錄加鎖時(shí),就可以復(fù)用之前為number值為15的記錄加鎖時(shí)生成的鎖結(jié)構(gòu)了。 -
至此今天的學(xué)習(xí)就到此結(jié)束了,愿您成為堅(jiān)不可摧的自己~~~
?
You can’t connect the dots looking forward; you can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future.You have to trust in something - your gut, destiny, life, karma, whatever. This approach has never let me down, and it has made all the difference in my life
如果我的內(nèi)容對(duì)你有幫助,請(qǐng) 點(diǎn)贊
、評(píng)論
、收藏
,創(chuàng)作不易,大家的支持就是我堅(jiān)持下去的動(dòng)力!文章來源:http://www.zghlxwxcb.cn/news/detail-481556.html
本文章參考:小孩子《MySQL是怎樣運(yùn)行的》文章來源地址http://www.zghlxwxcb.cn/news/detail-481556.html
到了這里,關(guān)于一文帶你了解MySQL之鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!