共計(jì) 9742 個字符,預(yù)計(jì)需要花費(fèi) 25 分鐘才能閱讀完成。
今天丸趣 TV 小編給大家分享一下 MySQL 鎖及分類有哪些的相關(guān)知識點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
1. 數(shù)據(jù)庫并發(fā)場景
在高并發(fā)場景下,不考慮其他中間件的情況下,數(shù)據(jù)庫會存在以下場景:
讀讀:不存在任何問題,也不需要并發(fā)控制。
讀寫:有線程安全問題,可能會造成事務(wù)隔離性問題,可能遇到臟讀,幻讀,不可重復(fù)讀。
寫寫:有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失。
針對以上問題,SQL 標(biāo)準(zhǔn)規(guī)定不同隔離級別下可能發(fā)生的問題不一樣:
MySQL 四大隔離級別:
隔離級別臟讀不可重復(fù)讀幻讀 READ UNCOMMITTED:未提交讀可能發(fā)生可能發(fā)生可能發(fā)生 READ COMMITTED:已提交讀解決可能發(fā)生可能發(fā)生 REPEATABLE READ:可重復(fù)讀解決解決可能發(fā)生 SERIALIZABLE:可串行化解決解決解決
可以看到,MySQL 在 REPEATABLE READ 隔離級別實(shí)際上就解決了不可重復(fù)度問題,基本解決了幻讀問題,但在極端情況下仍然存在幻讀現(xiàn)象。
那么有什么方式來解決呢?一般來說有兩種方案:
1?? 讀操作 MVCC,寫操作加鎖
對于讀,在 RR 級別的 MVCC 下,當(dāng)一個事務(wù)開啟的時候會產(chǎn)生一個 ReadView,然后通過 ReadView 找到符合條件的歷史版本,而這個版本則是由 undo 日志構(gòu)建的,而在生成 ReadView 的時候,其實(shí)就是生成了一個快照,所以此時的 SELECT 查詢也就是快照讀(或者一致性讀),我們知道在 RR 下,一個事務(wù)在執(zhí)行過程中只有第一次執(zhí)行 SELECT 操作才會生成一個 ReadView,之后的 SELECT 操作都復(fù)用這個 ReadView,這樣就避免了不可重復(fù)讀和很大程度上避免了幻讀的問題。
對于寫,由于在快照讀或一致性讀的過程中并不會對表中的任何記錄做加鎖操作并且 ReadView 的事務(wù)是歷史版本,而對于寫操作的最新版本兩者并不會沖突,所以其他事務(wù)可以自由的對表中的記錄做改動。
2?? 讀寫操作都加鎖
如果我們的一些業(yè)務(wù)場景不允許讀取記錄的舊版本,而是每次都必須去讀取記錄的最新版本,比方在銀行存款的事務(wù)中,你需要先把賬戶的余額讀出來,然后將其加上本次存款的數(shù)額,最后再寫到數(shù)據(jù)庫中。在將賬戶余額讀取出來后,就不想讓別的事務(wù)再訪問該余額,直到本次存款事務(wù)執(zhí)行完成,其他事務(wù)才可以訪問賬戶的余額。這樣在讀取記錄的時候也就需要對其進(jìn)行加鎖操作,這樣也就意味著讀操作和寫操作也像寫 - 寫操作那樣排隊(duì)執(zhí)行。
對于臟讀,是因?yàn)楫?dāng)前事務(wù)讀取了另一個未提交事務(wù)寫的一條記錄,但如果另一個事務(wù)在寫記錄的時候就給這條記錄加鎖,那么當(dāng)前事務(wù)就無法繼續(xù)讀取該記錄了,所以也就不會有臟讀問題的產(chǎn)生了。
對于不可重復(fù)讀,是因?yàn)楫?dāng)前事務(wù)先讀取一條記錄,另外一個事務(wù)對該記錄做了改動之后并提交之后,當(dāng)前事務(wù)再次讀取時會獲得不同的值,如果在當(dāng)前事務(wù)讀取記錄時就給該記錄加鎖,那么另一個事務(wù)就無法修改該記錄,自然也不會發(fā)生不可重復(fù)讀了。
對于幻讀,是因?yàn)楫?dāng)前事務(wù)讀取了一個范圍的記錄,然后另外的事務(wù)向該范圍內(nèi)插入了新記錄,當(dāng)前事務(wù)再次讀取該范圍的記錄時發(fā)現(xiàn)了新插入的新記錄,我們把新插入的那些記錄稱之為幻影記錄。
怎么理解這個范圍?如下:
假如表 user 中只有一條 id= 1 的數(shù)據(jù)。
當(dāng)事務(wù) A 執(zhí)行一個 id = 1 的查詢操作,能查詢出來數(shù)據(jù),如果是一個范圍查詢,如 id in(1,2),必然只會查詢出來一條數(shù)據(jù)。
此時事務(wù) B 執(zhí)行一個 id = 2 的新增操作,并且提交。
此時事務(wù) A 再次執(zhí)行 id in(1,2)的查詢,就會讀取出 2 條記錄,因此產(chǎn)生了幻讀。
注:由于 RR 可重復(fù)讀的原因,其實(shí)是查不出 id = 2 的記錄的,所以如果執(zhí)行一次 update … where id = 2,再去范圍查詢就能查出來了。
采用加鎖的方式解決幻讀問題就有不太容易了,因?yàn)楫?dāng)前事務(wù)在第一次讀取記錄時那些幻影記錄并不存在,所以讀取的時候加鎖就有點(diǎn)麻煩,因?yàn)椴⒉恢澜o誰加鎖。
那么 InnoDB 是如何解決的呢?我們先來看看 InnoDB 存儲引擎有哪些鎖。
2. MySQL 中的鎖及分類
在 MySQL 官方文檔 中,InnoDB 存儲引擎介紹了以下幾種鎖:
同樣,看起來仍然一頭霧水,但我們可以按照學(xué)習(xí) JDK 中鎖的方式來進(jìn)行分類:
3. 鎖的粒度分類
什么是鎖的粒度?所謂鎖的粒度就是你要鎖住的范圍是多大。
比如你在家上衛(wèi)生間,你只要鎖住衛(wèi)生間就可以了,不需要將整個家都鎖起來不讓家人進(jìn)門吧,衛(wèi)生間就是你的加鎖粒度。
怎樣才算合理的加鎖粒度呢?
其實(shí)衛(wèi)生間并不只是用來上廁所的,還可以洗澡,洗手。這里就涉及到優(yōu)化加鎖粒度的問題。
你在衛(wèi)生間里洗澡,其實(shí)別人也可以同時去里面洗手,只要做到隔離起來就可以,如果馬桶,浴缸,洗漱臺都是隔開相對獨(dú)立的(干濕分離了屬于是),實(shí)際上衛(wèi)生間可以同時給三個人使用,當(dāng)然三個人做的事兒不能一樣。這樣就細(xì)化了加鎖粒度,你在洗澡的時候只要關(guān)上浴室的門,別人還是可以進(jìn)去洗手的。如果當(dāng)初設(shè)計(jì)衛(wèi)生間的時候沒有將不同的功能區(qū)域劃分隔離開,就不能實(shí)現(xiàn)衛(wèi)生間資源的最大化使用。
同樣,在 MySQL 中也存在鎖的粒度。通常分為三種,行鎖,表鎖和頁鎖。
3.1 行鎖
在共享鎖和獨(dú)占鎖的介紹中其實(shí)都是針對某一行記錄的,所以也可以稱之為行鎖。
對一條記錄加鎖影響的也只是這條記錄而已,所以行鎖的鎖定粒度在 MySQL 中是最細(xì)的。InnoDB 存儲引擎默認(rèn)鎖就是行鎖。
它具有以下特點(diǎn):
鎖沖突概率最低,并發(fā)性高
由于行鎖的粒度小,所以發(fā)生鎖定資源爭用的概率也最小,從而鎖沖突的概率就低,并發(fā)性越高。
開銷大,加鎖慢
鎖是非常消耗性能的,試想一下,如果對數(shù)據(jù)庫的多條數(shù)據(jù)加鎖,必然會占用很多資源,而對于加鎖需要等待之前的鎖釋放才能加鎖。
會產(chǎn)生死鎖
關(guān)于什么是死鎖,可以往下看。
3.2 表鎖
表級鎖為表級別的鎖定,會鎖定整張表,可以很好的避免死鎖,也是 MySQL 中最大顆粒度的鎖定機(jī)制。
MyISAM 存儲引擎的默認(rèn)鎖就是表鎖。
它具有以下特點(diǎn):
開銷小,加鎖快
由于是對整張表加鎖,速度必然快于單條數(shù)據(jù)加鎖。
不會產(chǎn)生死鎖
都對整張表加鎖了,其他事務(wù)根本拿不到鎖,自然也不會產(chǎn)生死鎖。
鎖粒度大,發(fā)生鎖沖突概率大,并發(fā)性低
3.3 頁鎖
頁級鎖是 MySQL 中比較獨(dú)特的一種鎖定級別,在其他數(shù)據(jù)庫管理軟件中并不常見。
頁級鎖的顆粒度介于行級鎖與表級鎖之間,所以獲取鎖定所需要的資源開銷,以及所能提供的并發(fā)處理能力同樣也是介于上面二者之間。另外,頁級鎖和行級鎖一樣,會發(fā)生死鎖。
行鎖表鎖頁鎖鎖的粒度小大兩者之間加鎖效率慢快兩者之間沖突概率低高 - 并發(fā)性能高低一般性能開銷大小兩者之間是否死鎖是否是 4. 鎖的兼容性分類
在 MySQL 中數(shù)據(jù)的讀取主要分為當(dāng)前讀和快照讀:
快照讀
快照讀,讀取的是快照數(shù)據(jù),不加鎖的普通 SELECT 都屬于快照讀。
SELECT * FROM table WHERE ...
當(dāng)前讀
當(dāng)前讀就是讀的是最新數(shù)據(jù),而不是歷史的數(shù)據(jù),加鎖的 SELECT,或者對數(shù)據(jù)進(jìn)行增刪改都會進(jìn)行當(dāng)前讀。
SELECT * FROM table LOCK IN SHARE MODE;
SELECT FROM table FOR UPDATE;
INSERT INTO table values ...
DELETE FROM table WHERE ...
UPDATE table SET ...
而在大多數(shù)情況下,我們操作數(shù)據(jù)庫都是當(dāng)前讀的情形,而在并發(fā)場景下,既要允許讀 - 讀情況不受影響,又要使寫 - 寫、讀 - 寫或?qū)?- 讀情況中的操作相互阻塞,就需要用到 MySQL 中的共享鎖和獨(dú)占鎖。
4.1 共享鎖和獨(dú)占鎖
共享鎖(Shared Locks),也可以叫做讀鎖,簡稱 S 鎖。可以并發(fā)的讀取數(shù)據(jù),但是任何事務(wù)都不能對數(shù)據(jù)進(jìn)行修改。
獨(dú)占鎖(Exclusive Locks),也可以叫做排他鎖或者寫鎖,簡稱 X 鎖。若某個事物對某一行加上了排他鎖,只能這個事務(wù)對其進(jìn)行讀寫,在此事務(wù)結(jié)束之前,其他事務(wù)不能對其進(jìn)行加任何鎖,其他進(jìn)程可以讀取,不能進(jìn)行寫操作,需等待其釋放。
來分析一下獲取鎖的情形:假如存在事務(wù) A 和事務(wù) B
事務(wù) A 獲取了一條記錄的 S 鎖,此時事務(wù) B 也想獲取該條記錄的 S 鎖,那么事務(wù) B 也能獲取到該鎖,也就是說事務(wù) A 和事務(wù) B 同時持有該條記錄的 S 鎖。
如果事務(wù) B 想要獲取該記錄的 X 鎖,則此操作會被阻塞,直到事務(wù) A 提交之后將 S 鎖釋放。
如果事務(wù) A 首先獲取的是 X 鎖,則不管事務(wù) B 想獲取該記錄的 S 鎖還是 X 鎖都會被阻塞,直到事務(wù) A 提交。
因此,我們可以說 S 鎖和 S 鎖是兼容的,S 鎖和 X 鎖是不兼容的,X 鎖和 X 鎖也是不兼容的。
4.2 意向鎖
意向共享鎖(Intention Shared Lock),簡稱 IS 鎖。當(dāng)事務(wù)準(zhǔn)備在某條記錄上加 S 鎖時,需要先在表級別加一個 IS 鎖。
意向獨(dú)占鎖(Intention Exclusive Lock),簡稱 IX 鎖。當(dāng)事務(wù)準(zhǔn)備在某條記錄上加 X 鎖時,需要先在表級別加一個 IX 鎖。
意向鎖是表級鎖,它們的提出僅僅為了在之后加表級別的 S 鎖和 X 鎖時可以快速判斷表中的記錄是否被上鎖,以避免用遍歷的方式來查看表中有沒有上鎖的記錄。就是說其實(shí) IS 鎖和 IS 鎖是兼容的,IX 鎖和 IX 鎖是兼容的。
為什么需要意向鎖?
InnoDB 的意向鎖主要用戶多粒度的鎖并存的情況。比如事務(wù) A 要在一個表上加 S 鎖,如果表中的一行已被事務(wù) B 加了 X 鎖,那么該鎖的申請也應(yīng)被阻塞。如果表中的數(shù)據(jù)很多,逐行檢查鎖標(biāo)志的開銷將很大,系統(tǒng)的性能將會受到影響。
舉個例子,如果表中記錄 1 億,事務(wù) A 把其中有幾條記錄上了行鎖了,這時事務(wù) B 需要給這個表加表級鎖,如果沒有意向鎖的話,那就要去表中查找這一億條記錄是否上鎖了。如果存在意向鎖,那么假如事務(wù)A在更新一條記錄之前,先加意向鎖,再加X鎖,事務(wù) B 先檢查該表上是否存在意向鎖,存在的意向鎖是否與自己準(zhǔn)備加的鎖沖突,如果有沖突,則等待直到事務(wù)A釋放,而無須逐條記錄去檢測。事務(wù)B更新表時,其實(shí)無須知道到底哪一行被鎖了,它只要知道反正有一行被鎖了就行了。
說白了意向鎖的主要作用是處理行鎖和表鎖之間的矛盾,能夠顯示某個事務(wù)正在某一行上持有了鎖,或者準(zhǔn)備去持有鎖。
表級別的各種鎖的兼容性:
SISXIXS 兼容兼容不兼容不兼容 IS 兼容兼容不兼容不兼容 X 不兼容不兼容不兼容不兼容 IS 兼容兼容不兼容不兼容 4.3 讀操作的鎖
對于 MySQL 的讀操作,有兩種方式加鎖。
1?? SELECT * FROM table LOCK IN SHARE MODE
如果當(dāng)前事務(wù)執(zhí)行了該語句,那么它會為讀取到的記錄加 S 鎖,這樣允許別的事務(wù)繼續(xù)獲取這些記錄的 S 鎖(比方說別的事務(wù)也使用 SELECT … LOCK IN SHARE MODE 語句來讀取這些記錄),但是不能獲取這些記錄的 X 鎖(比方說使用 SELECT … FOR UPDATE 語句來讀取這些記錄,或者直接修改這些記錄)。
如果別的事務(wù)想要獲取這些記錄的 X 鎖,那么它們會阻塞,直到當(dāng)前事務(wù)提交之后將這些記錄上的 S 鎖釋放掉
2?? SELECT FROM table FOR UPDATE
如果當(dāng)前事務(wù)執(zhí)行了該語句,那么它會為讀取到的記錄加 X 鎖,這樣既不允許別的事務(wù)獲取這些記錄的 S 鎖(比方說別的事務(wù)使用 SELECT … LOCK IN SHARE MODE 語句來讀取這些記錄),也不允許獲取這些記錄的 X 鎖(比如說使用 SELECT … FOR UPDATE 語句來讀取這些記錄,或者直接修改這些記錄)。
如果別的事務(wù)想要獲取這些記錄的 S 鎖或者 X 鎖,那么它們會阻塞,直到當(dāng)前事務(wù)提交之后將這些記錄上的 X 鎖釋放掉。
4.4 寫操作的鎖
對于 MySQL 的寫操作,常用的就是 DELETE、UPDATE、INSERT。隱式上鎖,自動加鎖,解鎖。
1?? DELETE
對一條記錄做 DELETE 操作的過程其實(shí)是先在 B+ 樹中定位到這條記錄的位置,然后獲取一下這條記錄的 X 鎖,然后再執(zhí)行 delete mark 操作。我們也可以把這個定位待刪除記錄在 B+ 樹中位置的過程看成是一個獲取 X 鎖的鎖定讀。
2?? INSERT
一般情況下,新插入一條記錄的操作并不加鎖,InnoDB 通過一種稱之為隱式鎖來保護(hù)這條新插入的記錄在本事務(wù)提交前不被別的事務(wù)訪問。
3?? 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)行了。
PS:為什么上了寫鎖,別的事務(wù)還可以讀操作?
因?yàn)?InnoDB 有 MVCC 機(jī)制(多版本并發(fā)控制),可以使用快照讀,而不會被阻塞。
4. 鎖的粒度分類
什么是鎖的粒度?所謂鎖的粒度就是你要鎖住的范圍是多大。
比如你在家上衛(wèi)生間,你只要鎖住衛(wèi)生間就可以了,不需要將整個家都鎖起來不讓家人進(jìn)門吧,衛(wèi)生間就是你的加鎖粒度。
怎樣才算合理的加鎖粒度呢?
其實(shí)衛(wèi)生間并不只是用來上廁所的,還可以洗澡,洗手。這里就涉及到優(yōu)化加鎖粒度的問題。
你在衛(wèi)生間里洗澡,其實(shí)別人也可以同時去里面洗手,只要做到隔離起來就可以,如果馬桶,浴缸,洗漱臺都是隔開相對獨(dú)立的(干濕分離了屬于是),實(shí)際上衛(wèi)生間可以同時給三個人使用,當(dāng)然三個人做的事兒不能一樣。這樣就細(xì)化了加鎖粒度,你在洗澡的時候只要關(guān)上浴室的門,別人還是可以進(jìn)去洗手的。如果當(dāng)初設(shè)計(jì)衛(wèi)生間的時候沒有將不同的功能區(qū)域劃分隔離開,就不能實(shí)現(xiàn)衛(wèi)生間資源的最大化使用。
同樣,在 MySQL 中也存在鎖的粒度。通常分為三種,行鎖,表鎖和頁鎖。
4.1 行鎖
在共享鎖和獨(dú)占鎖的介紹中其實(shí)都是針對某一行記錄的,所以也可以稱之為行鎖。
對一條記錄加鎖影響的也只是這條記錄而已,所以行鎖的鎖定粒度在 MySQL 中是最細(xì)的。InnoDB 存儲引擎默認(rèn)鎖就是行鎖。
它具有以下特點(diǎn):
鎖沖突概率最低,并發(fā)性高
由于行鎖的粒度小,所以發(fā)生鎖定資源爭用的概率也最小,從而鎖沖突的概率就低,并發(fā)性越高。
開銷大,加鎖慢
鎖是非常消耗性能的,試想一下,如果對數(shù)據(jù)庫的多條數(shù)據(jù)加鎖,必然會占用很多資源,而對于加鎖需要等待之前的鎖釋放才能加鎖。
會產(chǎn)生死鎖
關(guān)于什么是死鎖,可以往下看。
4.2 表鎖
表級鎖為表級別的鎖定,會鎖定整張表,可以很好的避免死鎖,也是 MySQL 中最大顆粒度的鎖定機(jī)制。
MyISAM 存儲引擎的默認(rèn)鎖就是表鎖。
它具有以下特點(diǎn):
開銷小,加鎖快
由于是對整張表加鎖,速度必然快于單條數(shù)據(jù)加鎖。
不會產(chǎn)生死鎖
都對整張表加鎖了,其他事務(wù)根本拿不到鎖,自然也不會產(chǎn)生死鎖。
鎖粒度大,發(fā)生鎖沖突概率大,并發(fā)性低
4.3 頁鎖
頁級鎖是 MySQL 中比較獨(dú)特的一種鎖定級別,在其他數(shù)據(jù)庫管理軟件中并不常見。
頁級鎖的顆粒度介于行級鎖與表級鎖之間,所以獲取鎖定所需要的資源開銷,以及所能提供的并發(fā)處理能力同樣也是介于上面二者之間。另外,頁級鎖和行級鎖一樣,會發(fā)生死鎖。
行鎖表鎖頁鎖鎖的粒度小大兩者之間加鎖效率慢快兩者之間沖突概率低高 - 并發(fā)性能高低一般性能開銷大小兩者之間是否死鎖是否是 5. 算法實(shí)現(xiàn)分類
對于上面的鎖的介紹,我們實(shí)際上可以知道,主要區(qū)分就是在鎖的粒度上面,而 InnoDB 中用的鎖就是行鎖,也叫記錄鎖,但是要注意,這個記錄指的是通過給索引上的索引項(xiàng)加鎖。
InnoDB 這種行鎖實(shí)現(xiàn)特點(diǎn)意味著:只有通過索引條件檢索數(shù)據(jù),InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖。
不論是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對數(shù)據(jù)加鎖。
只有執(zhí)行計(jì)劃真正使用了索引,才能使用行鎖:即便在條件中使用了索引字段,但是否使用索引來檢索數(shù)據(jù)是由 MySQL 通過判斷不同執(zhí)行計(jì)劃的代價來決 定的,如果 MySQL 認(rèn)為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。
同時當(dāng)我們用范圍條件而不是相等條件檢索數(shù)據(jù),并請求鎖時,InnoDB 會給符合條件的已有數(shù)據(jù)記錄的索引項(xiàng)加鎖。
不過即使是行鎖,InnoDB 里也是分成了各種類型的。換句話說即使對同一條記錄加行鎖,如果類型不同,起到的功效也是不同的。通常有以下幾種常用的行鎖類型。
5.1 Record Lock
記錄鎖,單條索引記錄上加鎖。
Record Lock 鎖住的永遠(yuǎn)是索引,不包括記錄本身,即使該表上沒有任何索引,那么 innodb 會在后臺創(chuàng)建一個隱藏的聚集主鍵索引,那么鎖住的就是這個隱藏的聚集主鍵索引。
記錄鎖是有 S 鎖和 X 鎖之分的,當(dāng)一個事務(wù)獲取了一條記錄的 S 型記錄鎖后,其他事務(wù)也可以繼續(xù)獲取該記錄的 S 型記錄鎖,但不可以繼續(xù)獲取 X 型記錄鎖;當(dāng)一個事務(wù)獲取了一條記錄的 X 型記錄鎖后,其他事務(wù)既不可以繼續(xù)獲取該記錄的 S 型記錄鎖,也不可以繼續(xù)獲取 X 型記錄鎖。
5.2 Gap Locks
間隙鎖,對索引前后的間隙上鎖,不對索引本身上鎖。
MySQL 在 REPEATABLE READ 隔離級別下是可以解決幻讀問題的,解決方案有兩種,可以使用 MVCC 方案解決,也可以采用加鎖方案解決。但是在使用加鎖方案解決時有問題,就是事務(wù)在第一次執(zhí)行讀取操作時,那些幻影記錄尚 不存在,我們無法給這些幻影記錄加上記錄鎖。所以我們可以使用間隙鎖對其上鎖。
如存在這樣一張表:
CREATE TABLE test ( id INT (1) NOT NULL AUTO_INCREMENT,
number INT (1) NOT NULL COMMENT 數(shù)字 ,
PRIMARY KEY (id),
KEY number (number) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
# 插入以下數(shù)據(jù)
INSERT INTO test VALUES (1, 1);
INSERT INTO test VALUES (5, 3);
INSERT INTO test VALUES (7, 8);
INSERT INTO test VALUES (11, 12);
如下:
開啟一個事務(wù) A:
BEGIN;
SELECT * FROM test WHERE number = 3 FOR UPDATE;
此時,會對 ((1,1),(5,3)) 和((5,3),(7,8))之間上鎖。
如果此時在開啟一個事務(wù) B 進(jìn)行插入數(shù)據(jù),如下:
BEGIN;
# 阻塞
INSERT INTO test (id, number) VALUES (2,2);
結(jié)果如下:
為什么不能插入?因?yàn)橛涗?(2,2) 要 插入的話,在索引 number 上,剛好落在 ((1,1),(5,3)) 和((5,3),(7,8))之間,是有鎖的,所以不允許插入。如果在范圍外,當(dāng)然是可以插入的,如:
INSERT INTO test (id, number) VALUES (8,8);
5.3 Next-Key Locks
next-key locks 是索引記錄上的記錄鎖和索引記錄之前的間隙上的間隙鎖的組合,包括記錄本身,每個 next-key locks 是前開后閉區(qū)間,也就是說間隙鎖只是鎖的間隙,沒有鎖住記錄行,next-key locks 就是間隙鎖基礎(chǔ)上鎖住右邊界行。
默認(rèn)情況下,InnoDB 以 REPEATABLE READ 隔離級別運(yùn)行。在這種情況下,InnoDB 使用 Next-Key Locks 鎖進(jìn)行搜索和索引掃描,這可以防止幻讀的發(fā)生。
6. 樂觀鎖和悲觀鎖
樂觀鎖和悲觀鎖其實(shí)不算是具體的鎖,而是一種鎖的思想,不僅僅是在 MySQL 中體現(xiàn),常見的 Redis 等中間件都可以應(yīng)用這種思想。
6.1 樂觀鎖
所謂樂觀鎖,就是持有樂觀的態(tài)度,當(dāng)我們更新一條記錄時,假設(shè)這段時間沒有其他人來操作這條數(shù)據(jù)。
實(shí)現(xiàn)樂觀鎖常見的方式
常見的實(shí)現(xiàn)方式就是在表中添加 version 字段,控制版本號,每次修改數(shù)據(jù)后 +1。
在每次更新數(shù)據(jù)之前,先查詢出該條數(shù)據(jù)的 version 版本號,再執(zhí)行業(yè)務(wù)操作,然后在更新數(shù)據(jù)之前在把查到的版本號和當(dāng)前數(shù)據(jù)庫中的版本號作對比,若相同,則說明沒有其他線程修改過該數(shù)據(jù),否則作相應(yīng)的異常處理。
6.2 悲觀鎖
所謂悲觀鎖,就是持有悲觀的態(tài)度,一開始就假設(shè)改數(shù)據(jù)會被別人修改。
悲觀鎖的實(shí)現(xiàn)方式有兩種
共享鎖(讀鎖)和排它鎖(寫鎖),參考上面。
7. 死鎖
是指兩個或兩個以上的進(jìn)程在執(zhí)行過程中,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去。此時稱系統(tǒng) 處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖。
產(chǎn)生的條件
互斥條件:一個資源每次只能被一個進(jìn)程使用;
請求與保持條件:一個進(jìn)程因請求資源而阻塞時,對已獲得的資源保持不放;
不剝奪條件:進(jìn)程已獲得的資源,在沒有使用完之前,不能強(qiáng)行剝奪;
循環(huán)等待條件:多個進(jìn)程之間形成的一種互相循環(huán)等待的資源的關(guān)系。
MySQL 中其實(shí)也是一樣的,如下還是這樣一張表:
CREATE TABLE `user` (
`id` bigint NOT NULL COMMENT 主鍵 ,
`name` varchar(20) DEFAULT NULL COMMENT 姓名 ,
`sex` char(1) DEFAULT NULL COMMENT 性別 ,
`age` varchar(10) DEFAULT NULL COMMENT 年齡 ,
`url` varchar(40) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `suf_index_url` (`name`(3)) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
# 數(shù)據(jù)
INSERT INTO `user` (`id`, `name`, `sex`, `age`, `url`) VALUES ( 1 , a , 1 , 18 , https://javatv.net
INSERT INTO `user` (`id`, `name`, `sex`, `age`, `url`) VALUES (2 , b , 1 , 18 , https://javatv.net
按照如下順序執(zhí)行:
AB①BEGIN
②
BEGIN③SELECT * FROM user WHERE name= a FOR UPDATE
④
SELECT * FROM user WHERE name= b FOR UPDATE⑤SELECT * FROM user WHERE name= b FOR UPDATE
⑥
SELECT * FROM user WHERE name= a FOR UPDATE
1、開啟 A、B 兩個事務(wù);
2、首先 A 先查詢 name= a 的數(shù)據(jù),然后 B 也查詢 name= b 的數(shù)據(jù);
3、在 B 沒釋放鎖的情況下,A 嘗試對 name= b 的數(shù)據(jù)加鎖,此時會阻塞;
4、若此時,事務(wù) B 在沒釋放鎖的情況下嘗試對 name= a 的數(shù)據(jù)加鎖,則產(chǎn)生死鎖。
此時,MySQL 檢測到了死鎖,并結(jié)束了 B 中事務(wù)的執(zhí)行,此時,切回事務(wù) A,發(fā)現(xiàn)原本阻塞的 SQL 語句執(zhí)行完成了。可通過 show engine innodb status \G 查看死鎖。
如何避免
從上面的案例可以看出,死鎖的關(guān)鍵在于:兩個 (或以上) 的 Session 加鎖的順序不一致,所以我們在執(zhí)行 SQL 操作的時候要讓加鎖順序一致,盡可能一次性鎖定所需的數(shù)據(jù)行。
以上就是“MySQL 鎖及分類有哪些”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,丸趣 TV 小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注丸趣 TV 行業(yè)資訊頻道。