InnoDB之鎖

這里說(shuō)的鎖是指事務(wù)級(jí)別的對(duì)行記錄/表進(jìn)行加/解鎖。
事務(wù)的開始--加鎖,事務(wù)的提交/回滾--解鎖。
和我們通常說(shuō)的多線程對(duì)共享資源的鎖是不一樣的。

1. 鎖的類型

1. 行級(jí)鎖

我們知道InnoDB支持行級(jí)鎖。即以下兩種:

  • 共享鎖(S Lock):允許事務(wù)讀取某一行的數(shù)據(jù)
  • 排他鎖(X Lock):允許事務(wù)寫某一行的數(shù)據(jù)


    image.png

S鎖和S鎖是可以兼容的,S和X、X和X是不兼容的。

2. 表級(jí)鎖

InnoDB也有表級(jí)鎖,當(dāng)遇到如下SQL就會(huì)加表級(jí)鎖

ALTER TABLE, DROP TABLE, LOCK TABLES

LOCK TABLE my_tabl_name READ; 用讀鎖鎖表,會(huì)阻塞其他事務(wù)修改表數(shù)據(jù)。
LOCK TABLE my_table_name WRITE; 用寫鎖鎖表,會(huì)阻塞其他事務(wù)讀和寫。

即表級(jí)鎖也有S鎖和X鎖。

意向鎖

InnoDB中有一種表級(jí)鎖叫意向鎖。
InnoDB有一個(gè)這樣的規(guī)定:在加行級(jí)鎖之前自動(dòng)會(huì)加意向鎖。

  • 意向共享鎖(IS Lock):事務(wù)“想要”讀取某幾行數(shù)據(jù)。
  • 意向排他鎖(IX Lock):事務(wù)“想要”寫某幾行數(shù)據(jù)。

即在加S Lock時(shí),會(huì)先加IS Lock。在加X(jué) Lock時(shí),會(huì)先加IX Lock。
意向鎖只是表達(dá)一個(gè)意愿,表達(dá)后面我將要做什么。

那意向鎖的作用是什么呢?

考慮場(chǎng)景:我們使用LOCK TABLE語(yǔ)句希望對(duì)表A加讀鎖,這時(shí)我們應(yīng)該先要判斷是否有其他事務(wù)對(duì)表A進(jìn)行寫。

  • 沒(méi)有意向鎖
    做法是遍歷表A的所有行級(jí)鎖,看是否有X鎖。
    顯然這樣的方式太耗時(shí)。
  • 有了意向鎖
    做法判斷一下表A是否有IX就可以了。就可以判斷表A此時(shí)有沒(méi)有在寫的事務(wù)。

總結(jié)一下意向鎖的作用:就是為了提高鎖定表級(jí)數(shù)據(jù)的效率。

所以我們?cè)賮?lái)看一下意向鎖和表鎖之間的兼容性:


image.png
  • 意向鎖之間都是兼容的
  • 表中的S和X都指的是表級(jí)鎖。意向鎖和行級(jí)鎖不存在兼容性問(wèn)題,都是兼容的。

2. 一致性非鎖定讀

讀取數(shù)據(jù)時(shí),如果讀取的行已被加X(jué)鎖,則無(wú)需等待,讀取的是該行的快照版本。這種技術(shù)稱為多版本控制(MVVC)。


image.png
  • 快照版本可以有多個(gè),是由undo段來(lái)實(shí)現(xiàn),而undo段用于回滾,所以快照版本無(wú)需額外的開銷。
  • 讀取快照版本不會(huì)加任何鎖,因?yàn)闆](méi)有事務(wù)會(huì)對(duì)歷史版本進(jìn)行修改。

不同事務(wù)隔離下不同的行為

READ COMMITTED
  • 使用的是一致性非鎖定讀。
  • 快照版本是該行的最新一條快照版本。
REPEATABLE READ(默認(rèn)級(jí)別)
  • 使用的是一致性非鎖定讀。
  • 快照版本是事務(wù)開始時(shí)的版本。

這個(gè)行為也決定了RR為什么解決了不可重復(fù)讀的問(wèn)題。

解釋一下上面兩個(gè)快照版本的區(qū)別

事務(wù)A:讀行r | --------------- | 讀行r
事務(wù)B:--------| 寫r commit |

主要是看事務(wù)A第二次讀行r的結(jié)果

  • READ COMMITTED:讀的是事務(wù)B提交后的快照
  • REPEATABLE READ:讀的是事務(wù)A開始時(shí)讀的快照。

3. 一致性鎖定讀

意思就是用戶可以顯示地指定讀操作一定要加鎖。

select ...... for update :這條select語(yǔ)句加X(jué)鎖
select ......lock in share mode :這條select語(yǔ)句加S鎖

4. 自增長(zhǎng)與鎖

自增長(zhǎng)一般我們都會(huì)用,如何保證并發(fā)下插入的自增長(zhǎng)都是1?

  • InnoDB使用一種AUTO-INC Locking,它是一個(gè)表鎖。
    不同的是:這個(gè)鎖的釋放是在插入語(yǔ)句執(zhí)行結(jié)束后就釋放,而不是事務(wù)結(jié)束。所以這個(gè)效率就會(huì)還不錯(cuò)了。
    所以并不是全部的鎖都是在事務(wù)結(jié)束后才釋放。
  • 自增長(zhǎng)的列一定要加索引且是索引的第一列(如果索引是組合索引),否則MySQL會(huì)報(bào)錯(cuò)。

5. 行鎖的3種算法

Record Lock、Gap Lock和Next-Key Lock

  • Record Lock:?jiǎn)蝹€(gè)行記錄上的鎖。
  • Gap Lock:間隙鎖,鎖定一個(gè)范圍,不包含記錄本身
  • Next-Key Lock:Record Lock+Gap Lock,鎖定一個(gè)范圍且包含記錄本身。
    以上說(shuō)的鎖都是鎖的索引。如果沒(méi)有索引InnoDB會(huì)自動(dòng)建一個(gè)rowId索引。

舉個(gè)例子:
表A只有一個(gè)字段id,id為主鍵。此時(shí)表中有四行記錄1, 3, 5, 7
對(duì)于一個(gè)查詢語(yǔ)句而言:

Begin
select * from A where id > 2 for update;--加for update的目的是生成X鎖。
--注意此時(shí)事務(wù)并沒(méi)有提交
  • Record Lock:會(huì)鎖住這個(gè)sql語(yǔ)句掃描到的符合條件的所有索引。即3,5,7會(huì)被鎖住,即此時(shí)其他事務(wù)無(wú)法對(duì)id=3、5、7的行進(jìn)行查詢或修改。但可以插入id為4、6、8等行記錄。
  • Gap Lock:首先會(huì)根據(jù)所有的索引值生成Gap: (-無(wú)窮,1),(1, 3),(3,5),(5,7),(7,+無(wú)窮)。然后會(huì)鎖住根據(jù)sql語(yǔ)句掃描到的符合條件的所有g(shù)ap,不包含索引值本身。即此時(shí)其他事務(wù)可以修改記錄3、5、7但不可以插入4、6、8等。
  • Next-Key Lock:也是會(huì)先生成Gap:(-無(wú)窮,1],(1, 3],(3,5],(5,7],(7,+無(wú)窮)。注意這里的Gap是左開右閉的,即包含了掃描到的索引本身。所以此時(shí)會(huì)鎖住所有大于1的范圍,即其他事務(wù)無(wú)法寫入/修改和讀取所有大于1的記錄。

Next-Key Lock

Next-Key Lock是InnoDB在默認(rèn)隔離級(jí)別(REPEATABLE READ)下對(duì)于行查詢默認(rèn)的算法。設(shè)計(jì)的目的為了解決幻讀問(wèn)題。

幻讀

幻讀:相同兩個(gè)SQL查詢,由于兩次SQL查詢期間有其他事務(wù)的操作,導(dǎo)致執(zhí)行的結(jié)果不同。

  • 幻讀:第二次讀和第一次讀比較:多了一些行或少了一些行(其他事務(wù)插入/刪除了這些行記錄)
  • 不可重復(fù)讀:第二次讀和第一次讀比較:一些行的值不一樣(其他事務(wù)修改了這些行記錄)

從上面我們可以看到Next-Key Lock解決幻讀的具體過(guò)程。其實(shí)是for update+Next-Key Lock才可以解決幻讀現(xiàn)象。
這里我們總結(jié)一下原因:

  • for update:決定了讀必須加鎖
  • Next-Key Lock:決定了加的鎖是Gap+Record Lock。
  • 查詢SQL的條件:決定了Gap Lock的范圍。
降級(jí)

Next-Key Lock在一些情況下可以降級(jí)為Record Lock。

Begin
select * from A where id = 3 for update;
select * from A where id in (1,3) for update;

當(dāng)查詢的字段是唯一索引,且搜索的條件也是唯一的時(shí),此時(shí)Next-Key Lock就會(huì)降級(jí)為Record Lock。原因是根據(jù)這個(gè)條件查詢,不會(huì)出現(xiàn)因?yàn)槠渌聞?wù)insert導(dǎo)致同一個(gè)事務(wù)內(nèi)的兩次查詢,記錄不一樣。

  • REPEATABLE READ:使用next-key lock來(lái)解決這個(gè)問(wèn)題,即鎖住的是一個(gè)范圍。
  • READ COMMITTED:使用Record Lock,所以無(wú)法避免幻讀現(xiàn)象。
一個(gè)死鎖問(wèn)題-蠻有意思的
image.png
  • 在時(shí)間點(diǎn)4時(shí),會(huì)話A需要等待會(huì)話B的S鎖結(jié)束,所以這里會(huì)阻塞。
  • 時(shí)間點(diǎn)5時(shí),會(huì)話A需要等待會(huì)話B釋放S鎖,會(huì)話B需要等待會(huì)話A釋放S鎖,這樣就形成了循環(huán)等待,造成了死鎖。

InnoDB會(huì)檢測(cè)出這種死鎖條件,直接拋出異常。

6. 鎖的問(wèn)題

1. 臟讀

臟讀是指一個(gè)事務(wù)中讀到了其他事務(wù)未提交的數(shù)據(jù)。
一般不會(huì)出現(xiàn),除非隔離級(jí)別設(shè)為:READ UNCOMMITTED,其他隔離級(jí)別都不會(huì)出現(xiàn)。

2. 丟失更新

比如說(shuō)有一個(gè)這樣的邏輯:

[1] select `account` from a where `name` = 'kobe';
....
[2] update a 
set `account` = '[1]查詢結(jié)果 + 100' where `name` = `kobe`;
  • 首先查詢kobe的賬戶余額,然后根據(jù)賬戶余額去更新這個(gè)賬戶。
  • 出現(xiàn)的問(wèn)題:在[1]和[2]之間其他事務(wù)對(duì)kobe的賬戶做了改動(dòng),在執(zhí)行[2]時(shí),其實(shí)kobe的賬戶并不是[1]查到的結(jié)果了。
  • 比如:kobe賬戶總共1萬(wàn),在執(zhí)行[1]時(shí)查到1萬(wàn)。此時(shí)其他事務(wù)將賬戶減去9千,還剩一千,然后執(zhí)行[2],認(rèn)為賬戶余額還是1萬(wàn),則把賬戶設(shè)為1萬(wàn)+1百=1萬(wàn)1了。
  • 解決辦法:使用一致性鎖定讀,即在[1]讀的時(shí)候加X(jué)鎖,則其他事務(wù)就無(wú)法進(jìn)行修改和讀取,這樣[2]使用的余額就是數(shù)據(jù)庫(kù)中最新的值。
[1] select `account` from a where `name` = 'kobe' for update;
....
[2] update a 
set `account` = '[1]查詢結(jié)果 + 100' where `name` = `kobe`;

7. 數(shù)據(jù)庫(kù)隔離級(jí)別與讀問(wèn)題

image.png

1. 解釋一下

這個(gè)是數(shù)據(jù)庫(kù)系統(tǒng)的規(guī)范,即任何具體的數(shù)據(jù)庫(kù)都必須滿足的特性。只是不同的數(shù)據(jù)庫(kù)有自己的對(duì)這種規(guī)范的具體實(shí)現(xiàn)方式。

2. Lost updates

  • 即一個(gè)事務(wù)修改行r,另一個(gè)事務(wù)也可以修改行r,這樣后一個(gè)事務(wù)相當(dāng)于把前一個(gè)事務(wù)修改的數(shù)據(jù)覆蓋掉了。即前一個(gè)事務(wù)修改的數(shù)據(jù)“Lost”。
  • 所有的數(shù)據(jù)庫(kù)隔離級(jí)別都不會(huì)發(fā)生這種事,即行r被一個(gè)事務(wù)修改的時(shí)候,另一個(gè)事務(wù)是不可以修改的。
  • 上節(jié)說(shuō)的“丟失更新”是業(yè)務(wù)級(jí)別上的,并不是數(shù)據(jù)庫(kù)事務(wù)級(jí)別上的。

3. Dirty reads

  • 這個(gè)上面已經(jīng)說(shuō)過(guò)了,RC和RR都不會(huì)發(fā)生。RC通過(guò)名字就可以看出來(lái),read已經(jīng)committed的數(shù)據(jù)。
  • 在InnoDB中,RC由于一致性非限定讀,讀的是最新的快照版本,所有讀的一定是已經(jīng)committed的數(shù)據(jù)。

4. Non-repeatable reads

  • RR不會(huì)發(fā)生,通過(guò)名字就可以看出來(lái),repeatable read。
  • 在InnoDB中,RR由于一致性非限定讀,讀的是事務(wù)開始的快照版本,所以是repeatable read的。
  • 所以跟具體使用的哪種鎖(Next-Key Lock)沒(méi)有太大關(guān)系。其實(shí)個(gè)人感覺(jué)Next-Key Lock并沒(méi)有太大意義,可重復(fù)讀是靠MVVC解決的,而它只是在特定情況下能夠解決幻讀問(wèn)題,這個(gè)特定情況又是那么特定。

5. Phantoms

  • 由圖可以看出,只有Serializable才不會(huì)出現(xiàn)。
  • 在InnoDB中,從上面我們可以看到RR的Next-Key Lock說(shuō)可以解決幻讀現(xiàn)象,那與這里是否矛盾呢?
  • 其實(shí)并不矛盾,因?yàn)樯厦嬲f(shuō)RR的Next-Key Lock+for update才可以解決幻讀現(xiàn)象,只是單純的Next-Key Lock并不能解決幻讀現(xiàn)象。

8. 阻塞

當(dāng)一個(gè)事務(wù)等待另一個(gè)事務(wù)commit時(shí),我們稱為阻塞。
默認(rèn)情況下,當(dāng)一個(gè)事務(wù)阻塞到一定時(shí)間時(shí)(默認(rèn)50s),會(huì)拋出異常。注意此時(shí)此事務(wù)并沒(méi)有把之前的操作進(jìn)行commit或者roll back,此時(shí)處于一種非常危險(xiǎn)的狀態(tài)。所以一定要設(shè)置當(dāng)事務(wù)超時(shí)時(shí),是commit還是roll back。

9. 死鎖

回憶下死鎖的知識(shí):

數(shù)據(jù)庫(kù)中的死鎖是指:多個(gè)事務(wù)競(jìng)爭(zhēng)同一資源鎖,而出現(xiàn)互相等待,若無(wú)外力則無(wú)法進(jìn)行下去。

解決死鎖的辦法
  • 事務(wù)超時(shí)機(jī)制
    當(dāng)出現(xiàn)死鎖時(shí),事務(wù)阻塞時(shí)間超過(guò)某個(gè)值,則讓這個(gè)事務(wù)roll back釋放資源,這樣其他事務(wù)就可以繼續(xù)進(jìn)行下去。
    缺點(diǎn):roll back的事務(wù)永遠(yuǎn)是先阻塞的事務(wù),如果先阻塞的事務(wù)的undo太大,則代價(jià)很大。簡(jiǎn)單地說(shuō)就是無(wú)法控制應(yīng)該讓哪個(gè)事務(wù)roll back。
  • wait for graph
    維護(hù)一張有向圖,節(jié)點(diǎn)就是事務(wù),節(jié)點(diǎn)之間的連線說(shuō)明事務(wù)A需要等待事務(wù)B釋放資源。類似于下圖:


    image.png

    當(dāng)圖中出現(xiàn)回路時(shí),則說(shuō)明有死鎖。圖中的t1和t2就是一個(gè)回路。
    當(dāng)任意一個(gè)事務(wù)申請(qǐng)鎖時(shí),都回去更新這張圖,如果存在回路則挑undo量最小的事務(wù)進(jìn)行roll back。

死鎖的示例

死鎖的場(chǎng)景還是很簡(jiǎn)單的,就是A等待B,B等待A。就會(huì)出現(xiàn)死鎖。下面這個(gè)例子很簡(jiǎn)單,看一看。


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容