柚子快報(bào)邀請碼778899分享:數(shù)據(jù)庫 mysql中 什么是鎖
柚子快報(bào)邀請碼778899分享:數(shù)據(jù)庫 mysql中 什么是鎖
大家好。上篇文章我們講了事務(wù)并發(fā)執(zhí)行時可能帶來的各種問題,今天我們來聊一聊mysql面試必問的問題–鎖。
一、解決并發(fā)事務(wù)帶來問題的兩種基本方式
1. 并發(fā)事務(wù)訪問相同記錄的情況
并發(fā)事務(wù)訪問相同記錄的情況大致可以劃分為3種:
讀-讀情況: 即并發(fā)事務(wù)相繼讀取相同的記錄。
讀取操作本身不會對記錄有任何影響,并不會引起什么問題,所以允許這種情況的發(fā)生。
寫-寫情況: 即并發(fā)事務(wù)相繼對相同的記錄做出改動。
我們知道,在這種情況下會發(fā)生臟寫的問題,任何一種隔離級別都不允許這種問題的發(fā)生。所以在多個未提交事務(wù)相繼對一條記錄做改動時,需要讓它們排隊(duì)執(zhí)行,這個排隊(duì)的過程其實(shí)是通過鎖來實(shí)現(xiàn)的。 這個所謂的鎖其實(shí)是一個內(nèi)存中的結(jié)構(gòu),在事務(wù)執(zhí)行前本來是沒有鎖的,也就是說一開始是沒有鎖結(jié)構(gòu)和記錄進(jìn)行關(guān)聯(lián)的,如圖所示: 當(dāng)一個事務(wù)想對這條記錄做改動時,首先會看看內(nèi)存中有沒有與這條記錄關(guān)聯(lián)的鎖結(jié)構(gòu) ,當(dāng)沒有的時候就會在內(nèi)存中生成一個鎖結(jié)構(gòu)與之關(guān)聯(lián)。比方說事務(wù)T1要對這條記錄做改動,就需要生成一個鎖結(jié)構(gòu)與之關(guān)聯(lián): 其實(shí)在鎖結(jié)構(gòu)里有很多信息,為了簡化理解,我們現(xiàn)在只把兩個比較重要的屬性拿了出來:
trx信息: 代表這個鎖結(jié)構(gòu)是哪個事務(wù)生成的。 is_waiting: 代表當(dāng)前事務(wù)是否在等待。
如上圖所示,當(dāng)事務(wù)T1改動了這條記錄后,就生成了一個鎖結(jié)構(gòu)與該記錄關(guān)聯(lián),因?yàn)橹皼]有別的事務(wù)為這條記錄加鎖,所以is_waiting屬性就是false ,我們把這個場景就稱之為獲取鎖成功,或者加鎖成功,然后就可以繼續(xù)執(zhí)行操作了。
在事務(wù) T1 提交之前,另一個事務(wù)T2 也想對該記錄做改動,那么先去看看有沒有鎖結(jié)構(gòu)與這條記錄關(guān)聯(lián),發(fā)現(xiàn)有一個鎖結(jié)構(gòu)與之關(guān)聯(lián)后,然后也生成了一個鎖結(jié)構(gòu)與這條記錄關(guān)聯(lián),不過鎖結(jié)構(gòu)的is_waiting屬性值為 true,表示當(dāng)前事務(wù)需要等待,我們把這個場景就稱之為獲取鎖失敗,或者加鎖失敗,或者沒有成功的獲取到鎖,畫個圖表示就是這樣: 在事務(wù)T1提交之后,就會把該事務(wù)生成的鎖結(jié)構(gòu)釋放掉,然后看看還有沒有別的事務(wù)在等待獲取鎖,發(fā)現(xiàn)了事務(wù)T2還在等待獲取鎖,所以把事務(wù)T2對應(yīng)的鎖結(jié)構(gòu)的is_waiting屬性設(shè)置為false,然后把該事務(wù)對應(yīng)的線程喚醒,讓它繼續(xù)執(zhí)行,此時事務(wù)T2就算獲取到鎖了。效果圖就是這樣: 我們總結(jié)一下后續(xù)內(nèi)容中可能用到的幾種說法,以免大家混淆:
不加鎖:意思就是不需要在內(nèi)存中生成對應(yīng)的鎖結(jié)構(gòu),可以直接執(zhí)行操作。
獲取鎖成功,或者加鎖成功:意思就是在內(nèi)存中生成了對應(yīng)的鎖結(jié)構(gòu),而且鎖結(jié)構(gòu)的is_waiting屬性為false,也就是事務(wù)可以繼續(xù)執(zhí)行操作。
獲取鎖失敗,或者加鎖失敗,或者沒有獲取到鎖:意思就是在內(nèi)存中生成了對應(yīng)的 鎖結(jié)構(gòu) ,不過鎖結(jié)構(gòu)的 is_waiting 屬性為 true ,也就是事務(wù)需要等待,不可以繼續(xù)執(zhí)行操作。
讀-寫或?qū)?讀情況: 也就是一個事務(wù)進(jìn)行讀取操作,另一個進(jìn)行改動操作。
我們知道,這種情況下可能發(fā)生臟讀、不可重復(fù)讀、幻讀的問題。
注意:幻讀問題的產(chǎn)生是因?yàn)槟硞€事務(wù)讀了一個范圍的記錄,之后別的事務(wù)在該范圍內(nèi)插入了新記錄,該事務(wù)再次讀取該范圍的記錄時,可以讀到新插入的記錄,所以幻讀問題準(zhǔn)確的說并不是因?yàn)樽x取和寫入一條相同記錄而產(chǎn)生的。
怎么解決臟讀、不可重復(fù)讀、幻讀這些問題呢?其實(shí)有兩種可選的解決方案:
方案一:讀操作利用多版本并發(fā)控制( MVCC ),寫操作進(jìn)行加鎖。
所謂的MVCC就是通過生成一個ReadView,然后通過ReadView找到符合條件的記錄版本(歷史版本是由undo日志構(gòu)建的),查詢語句只能讀到在生成ReadView之前已提交事務(wù)所做的更改,在生成ReadView之前未提交的事務(wù)或者之后才開啟的事務(wù)所做的更改是看不到的。而寫操作肯定針對的是最新版本的記錄,讀記錄的歷史版本和改動記錄的最新版本本身并不沖突,也就是采用MVCC時, 讀-寫操作并不沖突。
我們說過普通的SELECT語句在READ COMMITTED和REPEATABLE READ隔離級別下會使用到MVCC讀取記錄。在READ COMMITTED隔離級別下,一個事務(wù)在執(zhí)行過程中每次執(zhí)行SELECT操作時都會生成一個ReadView,ReadView的存在本身就保證了事務(wù)不可以讀取到未提交的事務(wù)所做的更改,也就是避免了臟讀現(xiàn)象;REPEATABLE READ隔離級別下,一個事務(wù)在執(zhí)行過程中只有第一次執(zhí)行SELECT操作才會生成一個ReadView,之后的SELECT操作都復(fù)用這個ReadView,這樣也就避免了不可重復(fù)讀和幻讀的問題。
方案二:讀、寫操作都采用加鎖的方式。
如果我們的一些業(yè)務(wù)場景不允許讀取記錄的舊版本,而是每次都必須去讀取記錄的最新版本,比方在銀行存款的事務(wù)中,你需要先把賬戶的余額讀出來,然后將其加上本次存款的數(shù)額,最后再寫到數(shù)據(jù)庫中。在將賬戶余額讀取出來后,就不想讓別的事務(wù)再訪問該余額,直到本次存款事務(wù)執(zhí)行完成,其他事務(wù)才可以訪問賬戶的余額。這樣在讀取記錄的時候也就需要對其進(jìn)行加鎖操作,這樣也就意味著讀操作和寫操作也像寫-寫操作那樣排隊(duì)執(zhí)行。
我們說臟讀的產(chǎn)生是因?yàn)楫?dāng)前事務(wù)讀取了另一個未提交事務(wù)寫的一條記錄,如果另一個事務(wù)在寫記錄的時候就給這條記錄加鎖,那么當(dāng)前事務(wù)就無法繼續(xù)讀取該記錄了,所以也就不會有臟讀問題的產(chǎn)生了。
不可重復(fù)讀的產(chǎn)生是因?yàn)楫?dāng)前事務(wù)先讀取一條記錄,另外一個事務(wù)對該記錄做了改動之后并提交之后,當(dāng)前事務(wù)再次讀取時會獲得不同的值,如果在當(dāng)前事務(wù)讀取記錄時就給該記錄加鎖,那么另一個事務(wù)就無法修改該記錄,自然也不會發(fā)生不可重復(fù)讀了。
我們說幻讀問題的產(chǎn)生是因?yàn)楫?dāng)前事務(wù)讀取了一個范圍的記錄,然后另外的事務(wù)向該范圍內(nèi)插入了新記錄,當(dāng)前事務(wù)再次讀取該范圍的記錄時發(fā)現(xiàn)了新插入的新記錄,我們把新插入的那些記錄稱之為幻影記錄。采用加鎖的方式解決幻讀問題有一點(diǎn)麻煩,因?yàn)楫?dāng)前事務(wù)在第一次讀取記錄時那些幻影記錄并不存在,所以讀取的時候加鎖就會不知道給誰加鎖。我們稍后講解如何解決這個問題。
很明顯,采用MVCC方式的話, 讀-寫操作彼此并不沖突,性能更高,采用加鎖方式的話, 讀-寫操作彼此需要排隊(duì)執(zhí)行,影響性能。一般情況下我們當(dāng)然愿意采用MVCC來解決讀-寫操作并發(fā)執(zhí)行的問題,但是業(yè)務(wù)在某些特殊情況下,要求必須采用加鎖的方式執(zhí)行,那也是沒有辦法的事。
2. 一致性讀(Consistent Reads)
事務(wù)利用MVCC進(jìn)行的讀取操作稱之為一致性讀 ,或者一致性無鎖讀 ,有的地方也稱之為快照讀 。所有普通的SELECT語句( plain SELECT )在READ COMMITTED、 REPEATABLE READ隔離級別下都算是一致性讀 ,比方說:
SELECT * FROM t;
SELECT * FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2
一致性讀并不會對表中的任何記錄做加鎖操作,其他事務(wù)可以自由的對表中的記錄做改動。
3. 鎖定讀(Locking Reads)
1. 共享鎖和獨(dú)占鎖
我們前邊說過,并發(fā)事務(wù)的讀-讀情況并不會引起什么問題,不過對于 寫-寫、 讀-寫或?qū)?讀這些情況可能會引起一些問題,需要使用MVCC或者 加鎖的方式來解決它們。在使用加鎖的方式解決問題時,由于既要允許讀-讀情況不受影響,又要使寫-寫、讀-寫或?qū)?讀情況中的操作相互阻塞,所以MySQL 給鎖分了個類:
共享鎖,英文名:Shared Locks,簡稱S鎖。在事務(wù)要讀取一條記錄時,需要先獲取該記錄的S鎖 。
獨(dú)占鎖,也常稱排他鎖,英文名:Exclusive Locks,簡稱X鎖。在事務(wù)要改動一條記錄時,需要先獲取該記錄的X鎖 。
假如事務(wù)T1首先獲取了一條記錄的S鎖之后,事務(wù)T2接著也要訪問這條記錄:
如果事務(wù)T2想要再獲取一個記錄的S鎖,那么事務(wù)T2也會獲得該鎖,也就意味著事務(wù)T1和T2在該記錄上同時持有 S鎖 。
如果事務(wù)T2想要再獲取一個記錄的X鎖 ,那么此操作會被阻塞,直到事務(wù)T1提交之后將S鎖釋放掉。
如果事務(wù)T1首先獲取了一條記錄的X鎖之后,那么不管事務(wù)T2接著想獲取該記錄的S鎖還是X鎖都會被阻塞,直到事務(wù)T1提交。
所以我們說S鎖和S鎖是兼容的, S鎖和X鎖是不兼容的, X鎖和X鎖也是不兼容的。
2. 鎖定讀的語句
我們前邊說在采用加鎖方式解決臟讀、不可重復(fù)讀、幻讀這些問題時,讀取一條記錄時需要獲取一下該記錄的S鎖 ,其實(shí)這是不嚴(yán)謹(jǐn)?shù)?,有時候想在讀取記錄時就獲取記錄的X鎖 ,來禁止別的事務(wù)讀寫該記錄,為此MySQL提出了兩種比較特殊的SELECT語句格式:
對讀取的記錄加 S鎖 :
SELECT ... LOCK IN SHARE MODE;
也就是在普通的SELECT語句后邊加LOCK IN SHARE MODE,如果當(dāng)前事務(wù)執(zhí)行了該語句,那么它會為讀取到的記錄加S鎖 ,這樣允許別的事務(wù)繼續(xù)獲取這些記錄的S鎖,但是不能獲取這些記錄的X鎖。如果別的事務(wù)想要獲取這些記錄的X鎖 ,那么它們會阻塞,直到當(dāng)前事務(wù)提交之后將這些記錄上的S鎖釋放掉。
對讀取的記錄加X鎖 :
SELECT ... FOR UPDATE;
也就是在普通的SELECT語句后邊加FOR UPDATE,如果當(dāng)前事務(wù)執(zhí)行了該語句,那么它會為讀取到的記錄加X鎖 ,這樣既不允許別的事務(wù)獲取這些記錄的S鎖,也不允許獲取這些記錄的X鎖。如果別的事務(wù)想要獲取這些記錄的S鎖或者X鎖,那么它們會阻塞,直到當(dāng)前事務(wù)提交之后將這些記錄上的X鎖釋放掉。
關(guān)于更多 鎖定讀 的加鎖細(xì)節(jié)我們稍后會詳細(xì)嘮叨,稍安勿躁。
4. 寫操作
平常所用到的寫操作無非是DELETE、UPDATE、INSERT這三種:
DELETE: 對一條記錄做DELETE操作的過程其實(shí)是先在B+樹中定位到這條記錄的位置,然后獲取一下這條記錄的X鎖,然后再執(zhí)行delete mark 操作。我們也可以把這個定位待刪除記錄在B+樹中位置的過程看成是一個獲取X鎖的鎖定讀。
UPDATE: 在對一條記錄做UPDATE操作時分為三種情況:
如果未修改該記錄的鍵值并且被更新的列占用的存儲空間在修改前后未發(fā)生變化,則先在B+樹中定位到這條記錄的位置,然后再獲取一下記錄的X鎖,最后在原記錄的位置進(jìn)行修改操作。其實(shí)我們也可以把這個定位待修改記錄在B+樹中位置的過程看成是一個獲取X鎖的鎖定讀。
如果未修改該記錄的鍵值并且至少有一個被更新的列占用的存儲空間在修改前后發(fā)生變化,則先在B+樹中定位到這條記錄的位置,然后獲取一下記錄的X鎖,將該記錄徹底刪除掉(就是把記錄徹底移入垃圾鏈表),最后再插入一條新記錄。這個定位待修改記錄在B+樹中位置的過程看成是一個獲取X鎖的鎖定讀,新插入的記錄由 INSERT 操作提供的隱式鎖進(jìn)行保護(hù)。
如果修改了該記錄的鍵值,則相當(dāng)于在原記錄上做DELETE操作之后再來一次INSERT操作,加鎖操作就需要按照 DELETE 和 INSERT 的規(guī)則進(jìn)行了。
INSERT: 一般情況下,新插入一條記錄的操作并不加鎖,InnoDB通過一種稱之為隱式鎖的東西來保護(hù)這條新插入的記錄在本事務(wù)提交前不被別的事務(wù)訪問。
二、多粒度鎖
我們前邊提到的鎖都是針對記錄的,也可以被稱之為行級鎖或者行鎖 ,對一條記錄加鎖影響的也只是這條記錄而已,我們就說這個鎖的粒度比較細(xì);其實(shí)一個事務(wù)也可以在表級別進(jìn)行加鎖,自然就被稱之為表級鎖或者表鎖,對一個表加鎖影響整個表中的記錄,我們就說這個鎖的粒度比較粗。給表加的鎖也可以分為共享鎖( S鎖 )和 獨(dú)占鎖 ( X鎖 ):
給表加S鎖:如果一個事務(wù)給表加了S鎖,那么:
別的事務(wù)可以繼續(xù)獲得該表的S鎖 別的事務(wù)可以繼續(xù)獲得該表中的某些記錄的S鎖 別的事務(wù)不可以繼續(xù)獲得該表的X鎖 別的事務(wù)不可以繼續(xù)獲得該表中的某些記錄的X鎖
給表加X鎖:如果一個事務(wù)給表加了X鎖,那么:
別的事務(wù)不可以繼續(xù)獲得該表的S鎖 別的事務(wù)不可以繼續(xù)獲得該表中的某些記錄的S鎖 別的事務(wù)不可以繼續(xù)獲得該表的X鎖 別的事務(wù)不可以繼續(xù)獲得該表中的某些記錄的X鎖
那么我們在對表上鎖( 表鎖 )時,怎么知道表中有沒有記錄已經(jīng)被上鎖( 行鎖 )了呢?對此InnoDB提出了一種稱之為意向鎖(英文名:Intention Locks )的東西:
意向共享鎖,英文名:Intention Shared Lock,簡稱IS鎖 。當(dāng)事務(wù)準(zhǔn)備在某條記錄上加S鎖時,需要先在表級別加一個IS鎖 。
意向獨(dú)占鎖,英文名:Intention Exclusive Lock,簡稱IX鎖。當(dāng)事務(wù)準(zhǔn)備在某條記錄上加X鎖時,需要先在表級別加一個IX鎖 。
總結(jié)一下:IS、IX鎖是表級鎖,它們的提出僅僅為了在之后加表級別的S鎖和X鎖時可以快速判斷表中的記錄是否被上鎖,以避免用遍歷的方式來查看表中有沒有上鎖的記錄,也就是說其實(shí)IS鎖和IX鎖是兼容的,IX鎖和IX鎖是兼容的。我們畫個表來看一下表級別的各種鎖的兼容性:
兼容性XIXSISX不兼容不兼容不兼容不兼容IX不兼容兼容不兼容兼容S不兼容不兼容兼容兼容IS不兼容兼容兼容兼容
三、MySQL中的行鎖和表鎖
我們知道,MySQL支持多種存儲引擎,不同存儲引擎對鎖的支持也是不一樣的。我們重點(diǎn)討論InnoDB存儲引擎中的鎖,其他的存儲引擎我們稍微提一下
1. 其他存儲引擎中的鎖
對于MyISAM、MEMORY、 MERGE這些存儲引擎來說,它們只支持表級鎖,而且這些引擎并不支持事務(wù),所以使用這些存儲引擎的鎖一般都是針對當(dāng)前會話來說的。比方說在Session 1中對一個表執(zhí)行SELECT操作,就相當(dāng)于為這個表加了一個表級別的S鎖 ,如果在SELECT操作未完成時, Session 2中對這個表執(zhí)行UPDATE操作,相當(dāng)于要獲取表的X鎖,此操作會被阻塞,直到Session 1中的SELECT操作完成,釋放掉表級別的S鎖 后,Session 2中對這個表執(zhí)行UPDATE操作才能繼續(xù)獲取X鎖 ,然后執(zhí)行具體的更新語句。
因?yàn)槭褂肕yISAM、MEMORY、MERGE這些存儲引擎的表在同一時刻只允許一個會話對表進(jìn)行寫操作,所以這些存儲引擎實(shí)際上最好用在只讀,或者大部分都是讀操作,或者單用戶的情景下。另外,在MyISAM存儲引擎中有一個稱之為Concurrent Inserts的特性,支持在對MyISAM表讀取時同時插入記錄,這樣可以提升一些插入速度。
2. InnoDB存儲引擎中的鎖
InnoDB 存儲引擎既支持表鎖,也支持行鎖。表鎖實(shí)現(xiàn)簡單,占用資源較少,不過粒度很粗,有時候僅僅需要鎖住幾條記錄,但使用表鎖的話相當(dāng)于為表中的所有記錄都加鎖,所以性能比較差。行鎖粒度更細(xì),可以實(shí)現(xiàn)更精準(zhǔn)的并發(fā)控制。下邊我們詳細(xì)看一下。
1. InnoDB中的表級鎖
表級別的S鎖、X鎖:在對某個表執(zhí)行SELECT、INSERT、DELETE、 UPDATE語句時, InnoDB 存儲引擎是不會為這個表添加表級別的S鎖或者 X鎖的。
另外,在對某個表執(zhí)行一些諸如ALTER TABLE、DROP TABLE這類的 DDL語句時,其他事務(wù)對這個表并發(fā)執(zhí)行諸如SELECT、INSERT、 DELETE、UPDATE的語句會發(fā)生阻塞,同理,某個事務(wù)中對某個表執(zhí)行SELECT、INSERT、DELETE、UPDATE語句時,在其他會話中對這個表執(zhí)行DDL語句也會發(fā)生阻塞。這個過程其實(shí)是通過在server層使用一種稱之為元數(shù)據(jù)鎖(英文名:Metadata Locks,簡稱MDL)來實(shí)現(xiàn)的,一般情況下也不會使用InnoDB存儲引擎自己提供的表級別的S鎖和X鎖。
其實(shí)這個InnoDB存儲引擎提供的表級S鎖或者X鎖是相當(dāng)雞肋,只會在一些特殊情況下,比方說崩潰恢復(fù)過程中用到。不過我們還是可以手動獲取一下的,比方說在系統(tǒng)變量 autocommit=0,innodb_table_locks =1 時,手動獲取InnoDB存儲引擎提供的表t的S鎖或者X鎖可以這么寫:
LOCK TABLES t READ:InnoDB存儲引擎會對表t加表級別的S鎖 。
LOCK TABLES t WRITE:InnoDB存儲引擎會對表t加表級別的X鎖 。
不過請盡量避免在使用InnoDB存儲引擎的表上使用LOCK TABLES這樣的手動鎖表語句,它們并不會提供什么額外的保護(hù),只是會降低并發(fā)能力而已。
表級別的IS鎖、IX鎖:當(dāng)我們在對使用InnoDB存儲引擎的表的某些記錄加S鎖之前,那就需要先在表級別加一個IS鎖,當(dāng)我們在對使用InnoDB 存儲引擎的表的某些記錄加X鎖之前,那就需要先在表級別加一個IX鎖。IS鎖和IX鎖的使命只是為了后續(xù)在加表級別的S鎖和X鎖時判斷表中是否有已經(jīng)被加鎖的記錄,以避免用遍歷的方式來查看表中有沒有上鎖的記錄。
表級別的 AUTO-INC鎖:在使用MySQL過程中,我們可以為表的某個列添加 AUTO_INCREMENT 屬性,之后在插入記錄時,可以不指定該列的值,系統(tǒng)會自動為它賦上遞增的值,比方說我們有一個表:
CREATE TABLE t (
id INT NOT NULL AUTO_INCREMENT,
c VARCHAR(100),
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
由于這個表的id字段聲明了AUTO_INCREMENT,也就意味著在書寫插入語句時不需要為其賦值,比方說這樣:
INSERT INTO t(c) VALUES('aa'), ('bb');
上邊的插入語句并沒有為id列顯式賦值,所以系統(tǒng)會自動為它賦上遞增的值,效果就是這樣:
系統(tǒng)實(shí)現(xiàn)這種自動給 AUTO_INCREMENT修飾的列遞增賦值的原理主要是兩個:
采用 AUTO-INC鎖,也就是在執(zhí)行插入語句時就在表級別加一個 AUTO-INC 鎖,然后為每條待插入記錄的AUTO_INCREMENT修飾的列分配遞增的值,在該語句執(zhí)行結(jié)束后,再把 AUTO-INC鎖釋放掉。這樣一個事務(wù)在持有 AUTO-INC 鎖的過程中,其他事務(wù)的插入語句都要被阻塞,可以保證一個語句中分配的遞增值是連續(xù)的。
如果我們的插入語句在執(zhí)行前不可以確定具體要插入多少條記錄(無法預(yù)計(jì)即將插入記錄的數(shù)量),比方說使用 INSERT … SELECT 、 REPLACE … SELECT或者LOAD DATA這種插入語句,一般是使用AUTO-INC鎖為AUTO_INCREMENT修飾的列生成對應(yīng)的值。
采用一個輕量級的鎖,在為插入語句生成AUTO_INCREMENT修飾的列的值時獲取一下這個輕量級鎖,然后生成本次插入語句需要用到的 AUTO_INCREMENT列的值之后,就把該輕量級鎖釋放掉,并不需要等到整個插入語句執(zhí)行完才釋放鎖。
如果我們的插入語句在執(zhí)行前就可以確定具體要插入多少條記錄,比方說我們上邊舉的關(guān)于表 t 的例子中,在語句執(zhí)行前就可以確定要插入2條記錄,那么一般采用輕量級鎖的方式對 AUTO_INCREMENT 修飾的列進(jìn)行賦值。這種方式可以避免鎖定表,可以提升插入性能。
2. InnoDB中的行級鎖(重點(diǎn))
行鎖,也稱為記錄鎖 ,顧名思義就是在記錄上加的鎖。InnoDB把行鎖分成了各種類型。換句話說即使對同一條記錄加行鎖 ,如果類型不同,起到的功效也是不同的。為了方便講解,我們依然先創(chuàng)建一個表:
CREATE TABLE hero (
number INT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number),
KEY idx_name (name)
) Engine=InnoDB CHARSET=utf8;
我們主要是想用這個表存儲三國時的英雄,然后向這個表里插入幾條記錄:
INSERT INTO hero VALUES
(1, 'l劉備', '蜀'),
(3, 'z諸葛亮', '蜀'),
(8, 'c曹操', '魏'),
(15, 'x荀彧', '魏'),
(20, 's孫權(quán)', '吳');
現(xiàn)在表里的數(shù)據(jù)就是這樣的:
我們把hero表中的聚簇索引的示意圖畫一下:
當(dāng)然,我們把B+樹的索引結(jié)構(gòu)做了一個超級簡化,只把索引中的記錄給拿了出來,我們這里只是想強(qiáng)調(diào)聚簇索 引中的記錄是按照主鍵大小排序的,并且省略掉了聚簇索引中的隱藏列。現(xiàn)在準(zhǔn)備工作做完了,下邊我們來看看都有哪些常用的行鎖類型。
Record Locks: 我們前邊提到的記錄鎖就是這種類型,也就是僅僅把一條記錄鎖上,我們稱這種鎖為正經(jīng)記錄鎖。官方的類型名稱為:LOCK_REC_NOT_GAP。比方說我們把number值為8的那條記錄加一個這個鎖的示意圖如下:
正經(jīng)記錄鎖是有S鎖和X鎖之分的,我們分別稱之為S型正經(jīng)記錄鎖和X型正經(jīng)記錄鎖,當(dāng)一個事務(wù)獲取了一條記錄的S型正經(jīng)記錄鎖后,其他事務(wù)也可以繼續(xù)獲取該記錄的S型正經(jīng)記錄鎖,但不可以繼續(xù)獲取X型正經(jīng)記錄鎖;當(dāng)一個事務(wù)獲取了一條記錄的X型正經(jīng)記錄鎖后,其他事務(wù)既不可以繼續(xù)獲取該記錄的S型正經(jīng)記錄鎖,也不可以繼續(xù)獲取X型正經(jīng)記錄鎖。
Gap Locks: 我們說MySQL在REPEATABLE READ隔離級別下是可以解決幻讀問題的,解決方案有兩種,可以使用 MVCC 方案解決,也可以采用加鎖方案解決。但是在使用加鎖方案解決時有個大問題,就是事務(wù)在第一次執(zhí)行讀取操作時,那些幻影記錄尚不存在,我們無法給這些幻影記錄加上正經(jīng)記錄鎖。不過這難不倒InnoDB,他提出了一種稱之為Gap Locks 的鎖,官方的類型名稱為:LOCK_GAP,我們也可以簡稱為gap鎖。比方說我們把number值為8的那條記錄加一個gap鎖的示意圖如下:
如圖中為number值為8的記錄加了gap鎖 ,意味著不允許別的事務(wù)在number值為8的記錄前邊的間隙插入新記錄,其實(shí)就是number列的值 (3, 8) 這個區(qū)間的新記錄是不允許立即插入的。
比方說有另外一個事務(wù)再想插入一條number值為4的新記錄,它定位到該條新記錄的下一條記錄的number值為8,而這條記錄上又有一個gap鎖,所以就會阻塞插入操作,直到擁有這個gap鎖的事務(wù)提交了之后,number列的值在區(qū)間(3, 8)中的新記錄才可以被插入。
這個gap鎖的提出僅僅是為了防止插入幻影記錄而提出的,雖然有共享gap鎖和獨(dú)占gap鎖這樣的說法, 但是它們起到的作用都是相同的。而且如果你對一條記錄加了gap鎖(不論是共享gap鎖還是獨(dú)占gap 鎖),并不會限制其他事務(wù)對這條記錄加正經(jīng)記錄鎖或者繼續(xù)加gap鎖,再強(qiáng)調(diào)一遍,gap鎖的作用僅僅是為了防止插入幻影記錄的而已。
不知道大家發(fā)現(xiàn)了一個問題沒,給一條記錄加了gap鎖只是不允許其他事務(wù)往這條記錄前邊的間隙插入新記錄,那對于最后一條記錄之后的間隙,也就是hero表中number值為20的記錄之后的間隙該咋辦呢?也就是說給哪條記錄加gap鎖才能阻止其他事務(wù)插入number 值在(20, +∞) 這個區(qū)間的新記錄呢?
這時候應(yīng)該想起數(shù)據(jù)頁中的兩條偽記錄了:
Infimum記錄,表示該頁面中最小的記錄。
Supremum記錄,表示該頁面中最大的記錄。
為了實(shí)現(xiàn)阻止其他事務(wù)插入number值在 (20, +∞) 這個區(qū)間的新記錄,我們可以給索引中的最后一條記錄,也就是number值為20的那條記錄所在頁面的Supremum記錄加上一個gap鎖 ,畫個圖就是這樣:
Next-Key Locks: 有時候我們既想鎖住某條記錄,又想阻止其他事務(wù)在該記錄前邊的間隙插入新記錄,所以InnoDB就提出了一種稱之為Next-Key Locks的鎖,官方的類型名稱為:LOCK_ORDINARY,我們也可以簡稱為next-key鎖 。比方說我們把number值為8的那條記錄加一個 next-key鎖的示意圖如下:
next-key鎖的本質(zhì)就是一個正經(jīng)記錄鎖和一個gap鎖的合體,它既能保護(hù)該條記錄,又能阻止別的事務(wù)將新記錄插入被保護(hù)記錄前邊的間隙。
Insert Intention Locks: 我們說一個事務(wù)在插入一條記錄時需要判斷一下插入位置是不是被別的事務(wù)加了所謂的gap鎖(next-key 鎖也包含gap鎖),如果有的話,插入操作需要等待,直到擁有g(shù)ap鎖的那個事務(wù)提 交。但是InnoDB規(guī)定事務(wù)在等待的時候也需要在內(nèi)存中生成一個鎖結(jié)構(gòu),表明有事務(wù)想在某個間隙中插入新記錄,但是現(xiàn)在在等待。InnoDB就把這種類型的鎖命名為Insert Intention Locks ,官方的類型名稱為:LOCK_INSERT_INTENTION,我們也可以稱為插入意向鎖。比方說我們把number值為8的那條記錄加一個插入意向鎖的示意圖如下:
為了讓大家徹底理解這個插入意向鎖的功能,我們還是舉個例子然后畫個圖表示一下。比方說現(xiàn)在T1為number值為8的記錄加了一個gap鎖 ,然后T2和T3分別想向hero表中插入number值分別為4、5的兩條記錄,所以現(xiàn)在為number 值為8的記錄加的鎖的示意圖就如下所示:
從圖中可以看到,由于T1持有g(shù)ap鎖,所以T2和T3需要生成一個插入意向鎖的鎖結(jié)構(gòu)并且處于等待狀態(tài)。當(dāng)T1提交后會把它獲取到的鎖都釋放掉,這樣T2和T3就能獲取到對應(yīng)的插入意向鎖了(本質(zhì)上就是把插入意向鎖對應(yīng)鎖結(jié)構(gòu)的is_waiting屬性改為false),T2和T3之間也并不會相互阻塞,它們可以同時獲取到number值為8的插入意向鎖 ,然后執(zhí)行插入操作。事實(shí)上插入意向鎖并不會阻止別的事務(wù)繼續(xù)獲取該記錄上任何類型的鎖。
隱式鎖: 我們前邊說一個事務(wù)在執(zhí)行INSERT操作時,如果即將插入的間隙已經(jīng)被其他事務(wù)加了gap鎖,那么本次INSERT操作會阻塞,并且當(dāng)前事務(wù)會在該間隙上加一個插入意向鎖,否則一般情況下INSERT操作是不加 鎖的。那如果一個事務(wù)首先插入了一條記錄(此時并沒有與該記錄關(guān)聯(lián)的鎖結(jié)構(gòu)),然后另一個事務(wù)執(zhí)行如下操作:
立即使用SELECT … LOCK IN SHARE MODE語句讀取這條事務(wù),也就是在要獲取這條記錄的S鎖 ,或者使用SELECT … FOR UPDATE語句讀取這條事務(wù)或者直接修改這條記錄,也就是要獲取這條記錄的X鎖,該咋辦? 如果允許這種情況的發(fā)生,那么可能產(chǎn)生臟讀問題。
立即修改這條記錄,也就是要獲取這條記錄的X鎖,該咋辦?如果允許這種情況的發(fā)生,那么可能產(chǎn)生臟寫問題。
這時候事務(wù)id又要起作用了。我們把聚簇索引和二級索引中的記錄分開看一 下:
情景一:對于聚簇索引記錄來說,有一個trx_id隱藏列,該隱藏列記錄著最后改動該記錄的事務(wù)id。那么如果在當(dāng)前事務(wù)中新插入一條聚簇索引記錄后,該記錄的trx_id隱藏列代表的的就是當(dāng)前事務(wù)的事務(wù)id,如果其他事務(wù)此時想對該記錄添加S鎖或者X鎖時,首先會看一下該記錄的trx_id隱藏列代表的事務(wù)是否是當(dāng)前的活躍事務(wù),如果是的話,那么就幫助當(dāng)前事務(wù)創(chuàng)建一個X鎖(也就是為當(dāng)前事務(wù)創(chuàng)建一個鎖結(jié)構(gòu),is_waiting屬性是 false),然后自己進(jìn)入等待狀態(tài)(也就是為自己也創(chuàng)建一個鎖結(jié)構(gòu),is_waiting屬性是true)。
情景二:對于二級索引記錄來說,本身并沒有trx_id隱藏列,但是在二級索引頁面的Page Header部分有一個PAGE_MAX_TRX_ID屬性,該屬性代表對該頁面做改動的最大的事務(wù)id ,如果 PAGE_MAX_TRX_ID 屬性值小于當(dāng)前最小的活躍事務(wù)id ,那么說明對該頁面做修改的事務(wù)都已經(jīng)提交 了,否則就需要在頁面中定位到對應(yīng)的二級索引記錄,然后回表找到它對應(yīng)的聚簇索引記錄,然后再重復(fù)情景一的做法。通過上邊的敘述我們知道,一個事務(wù)對新插入的記錄可以不顯式的加鎖(生成一個鎖結(jié)構(gòu)),但是由于事務(wù)id的存在,相當(dāng)于加了一個隱式鎖。別的事務(wù)在對這條記錄加S鎖或者X鎖時,由于隱式鎖的存在,會先幫助當(dāng)前事務(wù)生成一個鎖結(jié)構(gòu),然后自己再生成一個鎖結(jié)構(gòu)后進(jìn)入等待狀態(tài)。
3. InnoDB鎖的內(nèi)存結(jié)構(gòu)
我們前邊說對一條記錄加鎖的本質(zhì)就是在內(nèi)存中創(chuàng)建一個鎖結(jié)構(gòu)與之關(guān)聯(lián),那么是不是一個事務(wù)對多條記錄加鎖,就要創(chuàng)建多個鎖結(jié)構(gòu)呢?比方說事務(wù)T1要執(zhí)行下邊這個語句:
# 事務(wù)T1
SELECT * FROM hero LOCK IN SHARE MODE;
很顯然這條語句需要為hero表中的所有記錄進(jìn)行加鎖,那是不是需要為每條記錄都生成一個鎖結(jié)構(gòu)呢?其實(shí)理論上創(chuàng)建多個鎖結(jié)構(gòu)沒問題,反而更容易理解,但是如果一個事務(wù)要獲取10000條記錄的鎖,要生成10000個這樣的結(jié)構(gòu)也太虧了吧!所以InnoDB決定在對不同記錄加鎖時,如果符合下邊這些條件:
在同一個事務(wù)中進(jìn)行加鎖操作;
被加鎖的記錄在同一個頁面中;
加鎖的類型是一樣的;
等待狀態(tài)是一樣的。
那么這些記錄的鎖就可以被放到一個鎖結(jié)構(gòu)中。我們畫個圖來看看 InnoDB 存儲引擎中的鎖結(jié)構(gòu):
我們看看這個結(jié)構(gòu)里邊的各種信息都是干嘛的:
鎖所在的事務(wù)信息: 不論是表鎖還是行鎖,都是在事務(wù)執(zhí)行過程中生成的,哪個事務(wù)生成了這個鎖結(jié)構(gòu),這里就記載著這個事務(wù)的信息。
索引信息: 對于行鎖來說,需要記錄一下加鎖的記錄是屬于哪個索引的。
表鎖/行鎖信息: 表鎖結(jié)構(gòu)和行鎖結(jié)構(gòu)在這個位置的內(nèi)容是不同的:
表鎖:記載著這是對哪個表加的鎖,還有其他的一些信息。
行鎖:記載了三個重要的信息:
Space ID:記錄所在表空間。
Page Number:記錄所在頁號。
n_bits:對于行鎖來說,一條記錄就對應(yīng)著一個比特位,一個頁面中包含很多記錄,用不同的比 特位來區(qū)分到底是哪一條記錄加了鎖。為此在行鎖結(jié)構(gòu)的末尾放置了一堆比特位,這個n_bits屬性代表使用了多少比特位。
type_mode: 這是一個32位的數(shù),被分成了lock_mode、lock_type和rec_lock_type三個部分,如圖所示:
鎖的模式(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鎖 。
鎖的類型(lock_type),占用第5~8位:
LOCK_TABLE(十進(jìn)制的 16),也就是當(dāng)?shù)?個比特位置為1時,表示表級鎖。
LOCK_REC(十進(jìn)制的 32),也就是當(dāng)?shù)?個比特位置為1時,表示行級鎖。
行鎖的具體類型(rec_lock_type),使用其余的位來表示。只有在 lock_type的值為LOCK_REC時, 也就是只有在該鎖為行級鎖時,才會被細(xì)分為更多的類型:
LOCK_ORDINARY(十進(jìn)制的 0):表示next-key鎖。
LOCK_GAP(十進(jìn)制的 512):也就是當(dāng)?shù)?0個比特位置為1時,表示 gap鎖。
LOCK_REC_NOT_GAP(十進(jìn)制的 1024 ):也就是當(dāng)?shù)?1個比特位置為1時,表示 正經(jīng)記錄鎖 。
LOCK_INSERT_INTENTION(十進(jìn)制的 2048):也就是當(dāng)?shù)?2個比特位置為1時,表示插入意向鎖。
其他的類型:還有一些不常用的類型我們就不多說了。
LOCK_WAIT(十進(jìn)制的 256):也就是當(dāng)?shù)?個比特位置為1時,表示 is_waiting為true,也就是當(dāng)前事務(wù)尚未獲取到鎖,處在等待狀態(tài);當(dāng)這個比特位為0時,表示is_waiting為false,也就是當(dāng)前事務(wù)獲取鎖成功。
注意:在InnoDB存儲引擎中,LOCK_IS,LOCK_IX,LOCK_AUTO_INC都算是表級鎖的模式,LOCK_S 和LOCK_X既可以算是表級鎖的模式,也可以是行級鎖的模式。
其他信息: 為了更好的管理系統(tǒng)運(yùn)行過程中生成的各種鎖結(jié)構(gòu)而設(shè)計(jì)了各種哈希表和鏈表。
一堆比特位: 如果是行鎖結(jié)構(gòu)的話,在該結(jié)構(gòu)末尾還放置了一堆比特位,比特位的數(shù)量是由上邊提到的n_bits屬性表示的。我們知道,頁面中的每條記錄在記錄頭信息中都包含一個heap_no屬性,偽記錄Infimum的 heap_no值為0,Supremum的heap_no值為1,之后每插入一條記錄, heap_no值就增1。鎖結(jié)構(gòu)最后的一堆比特位就對應(yīng)著一個頁面中的記錄,一個比特位映射一個heap_no,不過為了編碼方便,映射方式如下所示: 我們還是舉個例子說明一下。比方說現(xiàn)在有兩個事務(wù)T1和T2想對hero 表中的記錄進(jìn)行加鎖,hero表中記錄比較少,假設(shè)這些記錄都存儲在所在的表空間號為67,頁號為3的頁面上,那么如果:
T1想對number值為15 的這條記錄加S型正常記錄鎖,在對記錄加行鎖之前,需要先加表級別的IS鎖,也就是會生成一個表級鎖的內(nèi)存結(jié)構(gòu),接下來分析一下生成行鎖結(jié)構(gòu)的過程:
事務(wù)T1要進(jìn)行加鎖,所以鎖結(jié)構(gòu)的鎖所在事務(wù)信息指的就是T1。 直接對聚簇索引進(jìn)行加鎖,所以索引信息指的其實(shí)就是PRIMARY索引。 由于是行鎖,所以接下來需要記錄的是三個重要信息: Space ID:表空間號為67。 Page Number:頁號為3。 n_bits:我們的hero表中現(xiàn)在只插入了5條用戶記錄,但是在初始分配比特位時會多分配一些, 這主要是為了在之后新增記錄時不用頻繁分配比特位。其實(shí)計(jì)算n_bits有一個公式: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 是一個固定的值 64 ,所以本次加鎖的n_bits值就是:n_bits = (1 + ((7 + 64) / 8)) * 8 = 72 type_mode是由三部分組成的: lock_mode,這是對記錄加 S鎖 ,它的值為 LOCK_S。
lock_type,這是對記錄進(jìn)行加鎖,也就是行鎖,所以它的值為 LOCK_REC。
rec_lock_type,這是對記錄加正經(jīng)記錄鎖 ,也就是類型為 LOCK_REC_NOT_GAP的鎖。另外,由于當(dāng)前沒有其他事務(wù)對該記錄加鎖,所以應(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)閚umber值為15的記錄heap_no值為5 ,根據(jù)上邊列舉的比特位和heap_no的映射圖來看,應(yīng)該是第一個字節(jié)從低位往高位數(shù)第6個比特位被置為1,就像這樣:
綜上所述,事務(wù)T1為number 值為5的記錄加鎖生成的鎖結(jié)構(gòu)就如下圖所示:
T2 想對number值為3、8、15的這三條記錄加X型的next-key鎖,在對記錄加?鎖之前,需要先加表級別的 IX 鎖,也就是會?成?個表級鎖的內(nèi)存結(jié)構(gòu)。
現(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)不相同,所以這時候會?成兩個鎖結(jié)構(gòu)。這兩個鎖結(jié)構(gòu)中相同的屬性如下:
事務(wù) T2 要進(jìn)?加鎖,所以鎖結(jié)構(gòu)的鎖所在事務(wù)信息指的就是 T2 。 直接對聚簇索引進(jìn)?加鎖,所以索引信息指的其實(shí)就是PRIMARY 索引。 由于是?鎖,所以接下來需要記錄三個重要信息: Space ID:表空間號為 67。 Page Number:?號為 3。 n_bits:此屬性?成策略同T1中?樣,該屬性的值為 72。 type_mode是由三部分組成的: lock_mode,這是對記錄加 X 鎖,它的值為 LOCK_X。
lock_type,這是對記錄進(jìn)?加鎖,也就是?鎖,所以它的值為 LOCK_REC。
rec_lock_type,這是對記錄加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)?number 值為 3 、8的記錄heap_no值分別為3 、4,根據(jù)上邊列舉的?特位和heap_no的映射圖來看,應(yīng)該是第?個字節(jié)從低位往?位數(shù)第4、5個?特位被置為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)閚umber值為15的記錄heap_no值為5 ,根據(jù)上邊列舉的?特位和heap_no的映射圖來看,應(yīng)該是第?個字節(jié)從低位往?位數(shù)第6個?特位被置為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個鎖結(jié)構(gòu)。
好了,今天就講到這里了,到此#從根上理解mysql專欄也就結(jié)束了,最后在此聲明一下,本專欄是是作者按照下面這本書來寫的。
大家如果想要這本書的電子版可以私信我啊。也希望大家能給作者點(diǎn)個關(guān)注,謝謝大家!最后依舊是請各位老板有錢的捧個人場,沒錢的也捧個人場,謝謝各位老板!
柚子快報(bào)邀請碼778899分享:數(shù)據(jù)庫 mysql中 什么是鎖
好文鏈接
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。