繼續(xù)深入數(shù)據(jù)庫 了解一下數(shù)據(jù)庫的鎖機(jī)制
我們在高并發(fā)的場景下,經(jīng)常會在異常日志中看到“dead lock(死鎖)”的錯誤信息。想了無數(shù)的解決方案,都沒有能夠最終的解決,到底是什么原因引起了死鎖呢?要解決這個問題,我們就必須先了解透徹數(shù)據(jù)庫都有哪些鎖?他們的工作機(jī)制是什么樣的。
那,讓我們開啟今天的學(xué)習(xí)之路吧。
為什么數(shù)據(jù)庫要加鎖?
當(dāng)多條請求并發(fā)訪問一個數(shù)據(jù)庫資源時,有可能就會導(dǎo)致數(shù)據(jù)的不一致,因此,就需要一種機(jī)制來將數(shù)據(jù)庫的訪問順序化,從而保證數(shù)據(jù)庫數(shù)據(jù)的一致性,這個我們在
《數(shù)據(jù)庫常用的事務(wù)隔離級別都有哪些?都是什么原理?》也有講到。事務(wù)就是就是一種順序化的機(jī)制,而事務(wù)要達(dá)到目的,就必須要有所的支持。
數(shù)據(jù)庫都有哪些鎖?
由于數(shù)據(jù)庫的種類也不少,每種數(shù)據(jù)庫的鎖大致都相同,但是細(xì)節(jié)上略有不同,因此,我們就選擇MySql/InnoDB作為講解的對象。
InnoDB按照鎖的類型來劃分,主要分為了三個大類:共享鎖(Shared lock)、排它鎖,也叫獨占鎖(Exclusive Locks)、意向鎖(Intent Locks)。其中,意向鎖又分為了意向共享和意向排他,因此,嚴(yán)格意義上來說,是有四種分類的鎖,分別是:
- 共享鎖(Shared lock),簡稱:S鎖
- 排它鎖(Exclusive Locks),簡稱:X鎖
- 意向共享鎖(Intent Shared lock),簡稱:IS鎖
- 意向排他鎖(Intent Exclusive Locks),簡稱:IX鎖
接下來,我們就聊聊這些鎖都是怎么工作的。
共享鎖
共享鎖,顧名思義,就是我雖然鎖住了這個資源,但是我并不會獨占它,我同樣允許其他人使用這個資源。
通常情況下,查詢就是使用的共享鎖。
例如:
事務(wù)A先執(zhí)行了一個查詢
- select * from table;
事務(wù)A還沒有執(zhí)行完,事務(wù)B就執(zhí)行了另一個查詢
- select * from table where id = 1;
這個時候,事務(wù)B是可以優(yōu)先于事務(wù)A完成他的查詢的,并不存在必須要事務(wù)A結(jié)束,才執(zhí)行事務(wù)B的情況,這就是共享鎖的作用。
排它鎖
排它鎖,又叫獨占鎖,顧名思義,我鎖住了,這東西就是我一個人的,誰也別想看,別想碰。
通常情況下,修改操作就是使用的排它鎖。
例如:
事務(wù)A先執(zhí)行了一個修改操作
- update table set Status = 1;
事務(wù)B還事務(wù)A沒有完成時,執(zhí)行了另一個修改操作
- update table set Status = 0 where id =1;
這個時候,事務(wù)B就只有等著,到事務(wù)A執(zhí)行完成以后,事務(wù)B才能夠繼續(xù),這就是排它鎖的作用。
意向鎖
共享鎖、排它鎖按照其作用的粒度,可以鎖到行級,也可以鎖到頁級或表級。不過意向鎖只作用于表級,主要是用來標(biāo)記一個事務(wù)對于這張表操作的一個意向。
例如:我有一個事務(wù)需要使用表鎖,那我就需要知道,這個表是否存在其他的鎖,如果有,可能我就需要等待。但是,我如果要排除其他鎖,我就需要一個一個記錄的遍歷,才知道是不是存在行鎖。因此,數(shù)據(jù)庫對于行鎖就提出另一個機(jī)制,就是意向鎖,如果你要對這個表進(jìn)行行鎖時,那么先在表上加一個意向鎖,方便其他事務(wù)查詢。
因此,意向鎖就有了以下協(xié)議:
- 一個事務(wù)獲得表t中某行的S鎖之前,必須先獲得t表上的IS鎖或者更強(qiáng)類型的鎖。
- 一個事務(wù)獲得表t中某行的X鎖之前, 必須先獲得t表上的IX鎖。
現(xiàn)在我們知道了所的類型,接下來我們說說鎖的級別。
根據(jù)鎖的顆?;蛘呒墑e不同,我們又把所分為了三個級別:表鎖(table-level locking)、頁鎖(page-level locking)、行鎖(row-level locking)。
MyISAM和MEMORY存儲引擎采用的是表鎖(table-level locking);BDB存儲引擎采用的是頁鎖(page-level locking),但也支持表鎖;InnoDB存儲引擎既支持行鎖(row-level locking),也支持表鎖,但默認(rèn)情況下是采用行鎖。
而行鎖又包括了三種行鎖的算法,分別是:
- 記錄鎖(Record Lock)
- 間隙鎖(Gap Lock)
- 臨鍵鎖(Next-Key Lock)
這里有個小知識點:InnoDB的行鎖只針對索引項使用,也就是說,只有在通過索引檢索數(shù)據(jù)時,InnoDB才使用行鎖,其他時候都是使用的表鎖。
記錄鎖(Record Locks)
記錄鎖,顧名思義,就是鎖住一條記錄。這是Read Committed(讀提交)事務(wù)級別的默認(rèn)鎖級別。
記錄鎖是作用于索引的,所以,當(dāng)查詢不是作用于索引上時,系統(tǒng)會創(chuàng)建一個隱式的聚集索引,然后作用在索引上。
例如:
- select * from table where id = 1 lock in share mode;
就是一個共享記錄鎖,
- select * from table for update where id = 1 ;
就是一個排他記錄鎖。
間隙鎖(Gap Lock)
間隙鎖,它不會去鎖住索引本身,但是會鎖住的是一個索引的范圍。啟用它有一個前置條件,就是數(shù)據(jù)庫隔離級別必須是Repeatable Read(可重復(fù)讀),這也是InnoDB的默認(rèn)隔離級別,假設(shè)我們將隔離級別降到Read Committed(讀提交),間隙鎖將會自動失效。
間隙鎖的使用,能夠有效的防止幻讀。
例如:
如果事務(wù)A執(zhí)行了
- select * from table where id between 8 and 15 for update;
這是,事務(wù)B想在事務(wù)A執(zhí)行期間執(zhí)行插入一條id是10的記錄,就會被阻止。因為這會導(dǎo)致事務(wù)A中的多次查詢數(shù)據(jù)不一致。
臨鍵鎖(Next-Key Lock)
臨鍵鎖就是記錄鎖+間隙鎖的組合方式。這是Repeatable Read(可重復(fù)讀)隔離級別的默認(rèn)鎖級別。使用臨鍵鎖有一個好處,就是,假設(shè)我們執(zhí)行執(zhí)行一個查詢
- select * from table where id = 100;
如果id是***索引,那么臨鍵鎖就會降級為記錄鎖,鎖住這條記錄,而不是去鎖住一個范圍。
我們講完了這些鎖,那么就不禁要問了,死鎖是怎么產(chǎn)生的呢?
這就要說到另一個情況,就是鎖的升級。
鎖的升級
假設(shè),我們先進(jìn)行了一個查詢,找到了目標(biāo)數(shù)據(jù),然后進(jìn)行修改,在這個事務(wù)中,其實不同的階段,鎖的類型是不同的。
當(dāng)我們進(jìn)行查詢的時候,我們想數(shù)據(jù)庫先獲得了一個共享鎖,當(dāng)我們要對這條數(shù)據(jù)進(jìn)行更新的時候,并不是釋放共享鎖,然后再獲取排它鎖,而是進(jìn)行了一個鎖的升級操作,直接將共享鎖升級成為了排它鎖。
而就是因為這個操作,可能導(dǎo)致了死鎖。
死鎖
假設(shè):事務(wù)A中有一個鎖升級操作,也就是先執(zhí)行
- select * from table where id =1
再執(zhí)行
- update table set Status = 1 where id =1
事務(wù)B中,同樣存在這樣的情況,先執(zhí)行
- select * from table where id =1
再執(zhí)行
- update table set Name = '牛' where id =1
而執(zhí)行的順序恰好是:
- 事務(wù)A獲得了共享鎖,執(zhí)行查詢;
- 事務(wù)B獲得了共享鎖,執(zhí)行查詢;
- 事務(wù)A需要升級排它鎖,執(zhí)行修改;
- 事務(wù)B也需要升級排它鎖,不能釋放共享鎖。
于是,死鎖就發(fā)生了。當(dāng)然,還有一種交叉死鎖的情況,更為常見,大家可以自己百度看看了。
死鎖發(fā)生時,數(shù)據(jù)庫并不會直接檢查到死鎖的存在,只有在鎖等待超時的時候被發(fā)現(xiàn),然后殺死其中一個請求。如果并發(fā)量高時,死鎖就會引起大量的線程掛起,占用大量資源。
怎么預(yù)防死鎖呢?
最直接的辦法就是,別在update前先select一次。
但是,這種情況在所難免,很多時候我們update時,都是需要先select一次的,如果所有地方要求不能select,那代碼難度勢必就幾何級的上升。
還有一種辦法就是,加硬件,提高并發(fā)能力,這樣,出現(xiàn)兩次事務(wù)同時請求的概率就下降了。不過,這個方式成本太高。
當(dāng)然,我們還可以直接在查詢時,就申請到最終事務(wù)需要的鎖級別,避免升級鎖的出現(xiàn),也可以預(yù)防死鎖,例如查詢時就直接寫
- select * from table for update;