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