<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          完整版:Innodb到底是怎么加鎖的

          共 9603字,需瀏覽 20分鐘

           ·

          2021-11-18 14:59

          點(diǎn)擊上方“服務(wù)端思維”,選擇“設(shè)為星標(biāo)

          回復(fù)”669“獲取獨(dú)家整理的精選資料集

          回復(fù)”加群“加入全國服務(wù)端高端社群「后端圈」


          作者 | 小孩子4919
          出品?| 我們都是小青蛙


          不知道從什么時候開始,下邊這個錯誤的觀點(diǎn)開始被廣泛的流傳:

          在使用加鎖讀的方式讀取使用InnoDB存儲引擎的表時,當(dāng)在執(zhí)行查詢時沒有使用到索引時,行鎖會被轉(zhuǎn)換為表鎖。

          這里強(qiáng)調(diào)一點(diǎn),對于任何INSERTDELETEUPDATESELECT ... LOCK IN SHARE MODESELECT ... FOR UPDATE語句來說,InnoDB存儲引擎都不會加表級別的S鎖或者X鎖(我們這里不討論表級意向鎖的添加),只會加行級鎖。所以即使對于全表掃描的加鎖讀語句來說,也只會對表中的記錄進(jìn)行加鎖,而不是直接加一個表鎖。

          另外,很多小伙伴都會問:“這個語句加什么鎖”,其實這是一個偽命題,因為一個語句需要加什么鎖受到很多方面的影響,如果有人問你某某語句會加什么鎖,那你可以直接回懟:真不專業(yè)

          我們稍后給大家詳細(xì)分析一下影響加鎖的因素都有哪些,以及從源碼的角度看一下InnoDB到底是如何加鎖的,希望小伙伴看完后會驚呼:真tm的簡單!

          不過在進(jìn)行討論前我們需要申明一下,我們討論的只是InnoDB加的事務(wù)鎖,即為了避免臟寫臟讀不可重復(fù)讀幻讀這些現(xiàn)象帶來的一致性問題而加的鎖,并不是為了在多線程訪問共享內(nèi)存區(qū)域時而加的鎖(比方說兩個不同事務(wù)所在的線程想讀寫同一個頁面時,需要進(jìn)行加鎖保護(hù)),也不包括server層添加的MDL鎖。

          本文所參考的源碼版本為5.7.22

          事務(wù)鎖到底是什么

          是一個內(nèi)存結(jié)構(gòu),InnoDB中用lock_t這個結(jié)構(gòu)來定義:

          不論是行鎖,還是表鎖都用這個結(jié)構(gòu)來表示。我們給大家畫個圖:

          其中的type_mode是用于區(qū)分這個鎖結(jié)構(gòu)到底是行鎖還是表鎖,如果是表鎖的話是意向鎖、直接對表加鎖、還是AUTO-INC鎖,如果是行鎖的話,具體是正經(jīng)記錄鎖、gap鎖還是next-key鎖。

          小貼士:

          在InnoDB的實現(xiàn)中,InnoDB的行鎖是與記錄一一對應(yīng)的。即使是對于gap鎖來說,在實現(xiàn)上也是為某條記錄生成一個鎖結(jié)構(gòu),然后該鎖結(jié)構(gòu)的類型是gap鎖而已,并不是專門為某個區(qū)間生成一個鎖結(jié)構(gòu)。該gap鎖的功能就是每當(dāng)有別的事務(wù)插入記錄時,會檢查一下待插入記錄的下一條記錄上是否已經(jīng)有一個gap鎖的鎖結(jié)構(gòu),如果有的話就進(jìn)入阻塞狀態(tài)。

          我們平時所說的加鎖就是在內(nèi)存中生成這樣的一個鎖結(jié)構(gòu)(除了生成鎖結(jié)構(gòu),還有一種稱作隱式鎖的加鎖方式,不用生成鎖結(jié)構(gòu))。當(dāng)然,如果為1條記錄加鎖就要生成一個鎖結(jié)構(gòu),那豈不是太浪費(fèi)了!設(shè)計InnoDB的大叔提出了一種優(yōu)化方案,即同一個事務(wù),在同一個頁面上加的相同類型的鎖都放在同一個鎖結(jié)構(gòu)里。

          各種類型的鎖是如果通過type_mode區(qū)分、各種鎖都有什么作用,以及如何減少生成鎖結(jié)構(gòu)的細(xì)節(jié)我們這里就不展開了,那又要花費(fèi)超長的篇幅,大家可以到《MySQL是怎樣運(yùn)行的:從根兒上理解MySQL》書籍中查看,我們下邊來看具體的加鎖細(xì)節(jié)。

          準(zhǔn)備工作

          為了故事的順利發(fā)展,我們先創(chuàng)建一個表hero

          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)在hero表就有了兩個索引(一個二級索引,一個聚簇索引),示意圖如下:

          加鎖受哪些因素影響

          一條語句加什么鎖受多種因素影響,如果你不能確認(rèn)下邊這些因素的時候,最好不要搶先發(fā)言說"XXX語句對XXX記錄加了什么鎖":

          ?事務(wù)的隔離級別?語句執(zhí)行時使用的索引類型(比如聚簇索引、唯一二級索引、普通二級索引)?是否是精確匹配?是否是唯一性搜索?具體執(zhí)行的語句類型(SELECT、INSERT、DELETE、UPDATE)?是否開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量?記錄是否被標(biāo)記刪除

          這里邊有幾個概念大家可能不是很清楚,我們先解釋一下。

          掃描區(qū)間

          比方說下邊這個查詢:

          SELECT * FROM hero WHERE name <=  'l劉備' AND country = '魏';

          MySQL可以使用下邊兩種方式來執(zhí)行上述查詢:

          ?使用二級索引idx_name執(zhí)行上述查詢,那么就需要掃描name值在(-∞, 'l劉備']這個區(qū)間中的所有二級索引記錄,針對獲取到的每一條二級索引記錄,都需要執(zhí)行回表操作來獲取相應(yīng)的聚簇索引記錄。

          ?直接掃描所有的聚簇索引記錄,即進(jìn)行全表掃描。此時相當(dāng)于掃描number值在(-∞, +∞)這個區(qū)間中的所有聚簇索引記錄。

          優(yōu)化器會計算上述二種方式哪個成本更低,選用成本更低的那種來執(zhí)行查詢。

          當(dāng)優(yōu)化器使用二級索引執(zhí)行查詢時,我們把(-∞, 'l劉備']稱作掃描區(qū)間,意味著需要掃描name列值在這個區(qū)間中的所有二級索引記錄,我們也可以把形成這個掃描區(qū)間的條件name <= 'l劉備'稱作是形成這個掃描區(qū)間的邊界條件;當(dāng)優(yōu)化器使用全表掃描執(zhí)行查詢時,我們把(-∞, +∞)稱作掃描區(qū)間,意味著需要掃描number值在這個區(qū)間中的所有聚簇索引記錄。

          在執(zhí)行一個查詢的過程中,可能會用到多個掃描區(qū)間,如下所示:

          SELECT * FROM hero WHERE name < 'l劉備' OR name > 'x荀彧';

          如果優(yōu)化器采用二級索引idx_name執(zhí)行上述查詢時,那么對應(yīng)的掃描區(qū)間就是(-∞, l劉備)以及('x荀彧', +∞),即需要掃描name值在上述兩個掃描區(qū)間中的記錄。

          每當(dāng)InnoDB需要掃描一個掃描區(qū)間中的記錄時,都需要分兩步:

          ?先通過索引對應(yīng)的B+樹,從根頁面開始一路向下定位,直到定位到葉子節(jié)點(diǎn)中在掃描區(qū)間中的第一條記錄。

          ?之后就可以不需要繼續(xù)從根節(jié)點(diǎn)定位了,而是通過記錄的next_record屬性直接找到掃描區(qū)間的下一條記錄即可(頁面之間通過雙向鏈表連接,找完一個頁面中的記錄后,可以順著雙向鏈表再去下一個頁面中去找屬于同一個掃描區(qū)間的記錄)。

          也就是說在掃描某個掃描區(qū)間的記錄時,只有定位第1條記錄的時候稍微麻煩點(diǎn)兒,其他記錄只需要順著鏈表(單個頁面中的記錄連成一個單向鏈表,不同的頁面之間是雙向鏈表)掃描即可。

          精確匹配

          對于形成掃描區(qū)間的邊界條件來說,如果是等值匹配的條件,我們就把對這個掃描區(qū)間的匹配模式稱作精確匹配。比方說:

          SELECT * FROM hero WHERE name = 'l劉備' AND country = '魏';

          如果使用二級索引idx_name執(zhí)行上述查詢時,掃描區(qū)間就是['l劉備', 'l劉備'],形成這個掃描區(qū)間的邊界條件就是name = 'l劉備'。我們就把在使用二級索引idx_name執(zhí)行上述查詢時的匹配模式稱作精確匹配

          而對于下邊這個查詢來說

          SELECT * FROM hero WHERE name <=  'l劉備' AND country = '魏';

          顯然就不是精確匹配了。

          唯一性搜索

          如果在掃描某個掃描區(qū)間的記錄前,就能事先確定該掃描區(qū)間最多只包含1條記錄的話,那么就把這種情況稱作唯一性搜索。我們看一下代碼中判定掃描某個掃描區(qū)間的記錄是否是唯一性搜索的代碼是怎么寫的:

          其中:

          1.匹配模式是精確匹配2.使用的索引是聚簇索引或唯一二級索引3.如果索引中包含多個列,則每個列在生成掃描區(qū)間時都應(yīng)該被用到4.如果使用的索引是唯一二級索引,那么在搜索時不能搜索某個索引列為NULL的記錄(因為對于唯一二級索引來說,是可以存儲多個值為NULL的記錄的)。

          上邊幾點(diǎn)都比較好理解,我們稍微解釋一下第3點(diǎn)。比方說我們?yōu)槟硞€表的a、b兩列建立了一個唯一二級索引uk_a_b(a, b),那么對于搜索條件a=1形成的掃描區(qū)間來說,不能保證該掃描區(qū)間最多只包含一條記錄;對于搜索條件a=1 AND b= 1形成的掃描區(qū)間來說,才可以保證該掃描區(qū)間中僅包含1條記錄(不包括記錄的delete_flag=1的記錄)。

          row_search_mvcc

          我們知道MySQL其實是分成server層和存儲引擎層兩部分,每當(dāng)執(zhí)行一個查詢時,server層負(fù)責(zé)生成執(zhí)行計劃,即選取即將使用的索引以及對應(yīng)的掃描區(qū)間。我們這里以InnoDB為例,針對每一個掃描區(qū)間,都會:

          ?server層向InnoDB要掃描區(qū)間的第1條記錄

          ?InnoDB通過B+樹定位到掃描區(qū)間的第1條記錄(如果定位的是二級索引記錄并有回表需求則回表獲取完整的聚簇索引記錄),然后返回給server層

          ?server層判斷記錄是否符合搜索條件,如果符合則發(fā)送給客戶端,不符合則跳過。繼續(xù)向InnoDB要下一條記錄。

          小貼士:

          此處將記錄發(fā)送給客戶端其實是發(fā)送到本地的網(wǎng)絡(luò)緩沖區(qū),緩沖區(qū)大小由net_buffer_length控制,默認(rèn)是16KB大小。等緩沖區(qū)滿了才真正發(fā)送網(wǎng)絡(luò)包到客戶端。

          ?InnoDB根據(jù)記錄的單向鏈表以及頁面之間的雙向鏈表找到下一條記錄(如果定位的是二級索引記錄并有回表需求則回表獲取完整的聚簇索引記錄),返回給server層。

          ?server層處理該記錄,并向InnoDB要下一條記錄

          ?... 不停執(zhí)行上述過程,直到InnoDB讀到一條不符合邊界條件的記錄為止

          可見一般情況下,server層和存儲引擎層是以記錄為單位進(jìn)行通信的,而InnoDB讀取一條記錄最重要的函數(shù)就是row_search_mvcc

          可以看到這個函數(shù)長到嚇人,有一千多行。

          小貼士:

          不知道你們公司有沒有在一個函數(shù)中把業(yè)務(wù)邏輯寫到一千多行的同事,如果有的話你想不想打他。

          row_search_mvcc里,對一條記錄進(jìn)行諸如多版本的可見性判斷,要不要對記錄進(jìn)行加鎖的判斷,要是加鎖的話加什么鎖的選擇,完成記錄從InnoDB的存儲格式到server層存儲格式的轉(zhuǎn)換等等等等十分繁雜的工作。

          其實對于UPDATE、DELETE語句來說,執(zhí)行它們前都需要先在B+樹中定位到相應(yīng)的記錄,所以它們也會調(diào)用row_search_mvcc

          InnoDB對記錄的加鎖操作主要是在row_search_mvcc中的,像SELECT ... LOCK IN SHARE MODESELECT ... FOR UPDATEUPDATEDELETE這樣的語句都會調(diào)用row_search_mvcc完成加鎖操作。SELECT ... LOCK IN SHARE MODE會為記錄添加S型鎖,SELECT ... FOR UPDATEUPDATEDELETE會為記錄添加X型鎖。

          InnoDB每當(dāng)讀取一條記錄時,都會調(diào)用一次row_search_mvcc,在做了足夠長的鋪墊之后,我們終于可以看一下在row_search_mvcc函數(shù)中是怎么對某條記錄進(jìn)行加鎖的。

          語句到底是怎么加鎖的

          首先看一個十分重要的變量:

          set_also_gap_locks表示是否要給記錄添加gap鎖(next-key鎖可以看成是正經(jīng)記錄鎖和gap鎖的組合),它的默認(rèn)值是TRUE,表示默認(rèn)會給記錄添加gap鎖。

          set_also_gap_locks可能會在下邊這個地方發(fā)生變化:

          即如果當(dāng)前執(zhí)行的是SELECT ... LOCK IN SHARE MODE或者SELECT ... FOR UPDATE這樣的加鎖讀語句(非DELETE或UPDATE語句),并且隔離級別不大于READ COMMITTED 時,將set_also_gap_locks設(shè)置為FALSE。

          其中prebuilt->select_lock_type表示加鎖的類型,LOCK_NONE表示不加鎖,LOCK_S表示加S鎖(比方說執(zhí)行SELECT ... LOCK IN SHARE MODE時),LOCK_X表示加X鎖(比方說執(zhí)行SELECT ... FOR UPDATE、DELETE、UPDATE時)。

          對普通的SELECT的處理和意向鎖的添加

          再往后看:

          其中:

          ?標(biāo)號1的箭頭是對普通的SELECT的處理,在查詢開啟前需要生成ReadView。

          小貼士:

          具體的講就是對于Repeatable Read隔離級別來說,只在首次執(zhí)行SELECT語句時生成Readview,之后的SELECT語句都復(fù)用這個ReadView;對于Read Committed隔離級別來說,每次執(zhí)行SELECT語句時都會生成一個ReadView。這一點(diǎn)并不是在上邊截圖中的代碼里實現(xiàn)的。

          ?標(biāo)號2的箭頭是對加鎖讀的語句的處理,在首次讀取記錄(prebuilt->sql_stat_start表示是否是首次讀取)前,需要添加表級別的意向鎖(IS或IX鎖)。

          下邊是真正處理記錄并給記錄加鎖的流程,我們給這些流程編個號。

          1. 定位掃描區(qū)間的第一條記錄

          下邊開始通過B+樹定位某個掃描區(qū)間中的第一條記錄了(對于一個掃描區(qū)間來說,只執(zhí)行一次下述函數(shù),因為只要定位到掃描區(qū)間的第一條記錄之后,就可以沿著記錄所在的單向鏈表進(jìn)行查詢了):

          其中btr_pcur_open_with_no_init是用于定位掃描區(qū)間中的第一條記錄的函數(shù)。

          2. 對于ORDER BY ... DESC條件形成的掃描區(qū)間的第一條記錄的處理

          在B+樹的每層節(jié)點(diǎn)中,記錄是按照鍵值從小到大的方式進(jìn)行排序的。對于某個掃描區(qū)間來說,InnoDB通常是定位到掃描區(qū)間中最左邊的那條記錄,也就是鍵值最小的那條記錄,然后沿著從左往右的方式向后掃描。

          但是對于下邊這個查詢來說:

          SELECT * FROM hero WHERE name < 's孫權(quán)' AND country = '魏' ORDER BY name DESC FOR UPDATE ;

          如果優(yōu)化器決定使用二級索引idx_name執(zhí)行上述查詢的話,那么對應(yīng)的掃描區(qū)間就是(-∞, 's孫權(quán)')。由于上述查詢要求記錄是按照從大到小的順序返回給用戶,所以InnoDB定位到掃描區(qū)間中的第一條記錄應(yīng)該是該掃描區(qū)間中最右邊的那條記錄,也就是鍵值最大的那條記錄(在執(zhí)行btr_pcur_open_with_no_init時就定位到最右邊的那條記錄),我們看一下idx_name二級索引示意圖:

          很顯然,name值為'l劉備'的二級索引記錄是掃描區(qū)間(-∞, 's孫權(quán)')中最右邊的記錄。

          對于從右向左掃描掃描區(qū)間中記錄的情況,針對從掃描區(qū)間中定位到的最右邊的那條記錄,需要做如下處理:

          其中sel_set_rec_lock就是對一條記錄進(jìn)行加鎖的函數(shù)。

          可以看到,對于加鎖讀來說,在隔離級別不小于REPEATABLE READ并且也沒有開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量的情況下,會對掃描區(qū)間中最右邊的那條記錄的下一條記錄加一個類型為LOCK_GAP的鎖,這個類型為LOCK_GAP的鎖其實就是gap鎖

          在本例中,假設(shè)事務(wù)的隔離級別是REPATABLE READ。掃描區(qū)間(-∞, 's孫權(quán)')中最右邊的那條記錄就是name值為'l劉備'的二級索引記錄,接下來就應(yīng)該為該記錄的下一條記錄,也就是name值為's孫權(quán)'的二級索引記錄加一個gap鎖。

          小貼士:

          大家可以讀一下上述代碼的注釋,其實這樣加鎖主要是為了阻止幻讀。另外,這一步驟的加鎖僅僅針對從右向左的掃描區(qū)間中的最右邊的那條記錄,之后掃描該掃描區(qū)間中的其他記錄時就不做這一步的操作了。

          3. 真正的加鎖流程才開始——對Infimum和Supremum記錄的處理

          步驟1是用來定位掃描區(qū)間中的第一條記錄,針對一個掃描區(qū)間只執(zhí)行1次。

          步驟2是針對從右向左掃描的掃描區(qū)間中最右邊的那條記錄的下一條記錄進(jìn)行加鎖,針對一個掃描區(qū)間也執(zhí)行1次。

          從第3步驟開始以及往后的步驟,掃描區(qū)間中的每一條記錄都要經(jīng)歷。

          先看一下如果當(dāng)前記錄是Infimum記錄或者Supremum記錄時的處理:

          從上邊的代碼中可以看出,如果當(dāng)前讀取的記錄是Infimum記錄,則啥也不做,直接去讀下一條記錄。

          如果當(dāng)前讀取的記錄是Supremum記錄,則在下邊這些條件成立的時候就會為記錄添加一個類型為LOCK_ORDINARY的鎖,其實也就是next-key鎖

          ?set_also_gap_locks是TRUE(這個變量只在前邊設(shè)置過,當(dāng)隔離級別不大于READ COMMITTED的SELECT語句的加鎖讀會設(shè)置為FALSE,否則為TRUE)

          ?未開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量并且事務(wù)的隔離級別不小于REPEATABLE READ。

          ?本次讀取屬于加鎖讀

          ?所使用的不是空間索引。

          其實由于Supremum記錄本身是一條偽記錄,別的事務(wù)并不會更新或刪除它,所以給它添加next-key鎖起到的效果和給它添加gap鎖是一樣的。

          小貼士:

          Infimum記錄和Supremum記錄是InnoDB自動為B+樹中的每個頁面都添加的兩條虛擬記錄,也可以被稱作偽記錄。Infimum記錄和Supremum記錄分別占用13字節(jié)的存儲空間,被放置在頁面中固定的位置。其中Infimum記錄被看作最小的記錄,Supremum記錄被看作最大的記錄,Infimum記錄屬于頁面中的記錄單向鏈表的頭節(jié)點(diǎn),Supremum記錄屬于頁面中的記錄單向鏈表的尾節(jié)點(diǎn)。更多關(guān)于頁面結(jié)構(gòu)的內(nèi)容小伙伴們可以參考《MySQL是怎樣運(yùn)行的:從根兒上理解MySQL》書籍哈~

          4. 真正的加鎖流程才開始——對精確匹配的特殊處理

          如果當(dāng)前記錄不是Infimum記錄或者Supremum記錄,下邊進(jìn)入對匹配模式是精確匹配的一個特殊處理:

          可以看到,對于匹配模式是精確匹配的掃描區(qū)間來說,如果執(zhí)行本次row_search_mvcc獲取到的記錄不在掃描區(qū)間中(0 != cmp_dtuple_rec(search_tuple, rec, offsets)),則需要進(jìn)行一些特殊處理,即:

          對于加鎖讀來說,如果事務(wù)的隔離級別不小于Repeatable Read并且未開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量,那么就對該記錄加一個gap鎖,并且直接返回(代碼中直接跳轉(zhuǎn)到normal_return處),就不進(jìn)行后續(xù)的加鎖操作了。

          我們舉一個例子,比方說當(dāng)前事務(wù)的隔離級別為Repeatable Read,執(zhí)行如下語句:

          SELECT * FROM hero WHERE name = 's孫權(quán)' FOR UPDATE;

          如果使用二級索引idx_name執(zhí)行上述查詢,那么對應(yīng)的掃描區(qū)間就是['s孫權(quán)', 's孫權(quán)']。該語句會首先對name值是's孫權(quán)'的記錄進(jìn)行加鎖,不過該記錄是在掃描區(qū)間中的,上述代碼并不處理這種正常情況,關(guān)于正常情況的加鎖我們稍后分析。

          當(dāng)讀取完's孫權(quán)'的記錄后,InnoDB會根據(jù)記錄的next_record屬性找到下一條二級索引記錄,即name值為'x荀彧'的二級索引記錄,該記錄不在掃描區(qū)間['s孫權(quán)', 's孫權(quán)']中,即符合?0 != cmp_dtuple_rec(search_tuple, rec, offsets)條件,那么就執(zhí)行上述代碼的加鎖流程 —— 對name值為'x荀彧'的二級索引記錄加一個gap鎖。另外,err被賦值為DB_RECORD_NOT_FOUND,這意味著向server層報告當(dāng)前掃描區(qū)間的記錄都已經(jīng)掃描完了,server層在收到這個信息后就會停止向Innodb索要下一條記錄的請求,即結(jié)束本掃描區(qū)間的查詢。

          小貼士:

          這一步驟是對精確匹配的掃描區(qū)間的一個特殊處理,即當(dāng)server層收到InnoDB返回的掃描區(qū)間的最后一條記錄,server層仍會向InnoDB索要下一條記錄。InnoDB仍會沿著記錄所在的鏈表向后讀取,此次讀取到的記錄就不在掃描區(qū)間中了,如例子中的name值為'x荀彧'的二級索引記錄就不在掃描區(qū)間['s孫權(quán)', 's孫權(quán)']中。如果這是一個精確匹配的掃描區(qū)間,那么就進(jìn)行如步驟4所示的特殊處理,如果不是的話,就繼續(xù)執(zhí)行第5步,也就是走正常的加鎖流程。

          5. 真正的加鎖流程才開始——這回真的開始了

          我們在代碼中畫了2個紅框,這兩個紅框是對記錄是不對記錄加gap鎖的場景。我們具體看一下。

          對于1號紅框來說:

          ?set_also_gap_locks是FALSE(這個變量只在前邊設(shè)置過,當(dāng)隔離級別不大于READ COMMITTED的SELECT語句的加鎖讀會設(shè)置為FALSE,否則為TRUE)

          ?開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量

          ?事務(wù)的隔離級別不大于READ COMMITTED

          ?唯一性搜索并且該記錄的delete_flag不為1

          ?該索引是空間索引

          也就是說只要上邊任意一個條件成立,該記錄就不應(yīng)該被加gap鎖,而應(yīng)該添加正經(jīng)記錄鎖。其余情況就應(yīng)該加next-key鎖(gap鎖和正經(jīng)記錄鎖的合體)了。

          緊接著2號紅框就又?jǐn)⑹隽艘粋€不加gap鎖的場景:

          對于>= 主鍵的這種邊界條件來說,如果當(dāng)前記錄恰好是開始邊界,就僅需對該記錄加正經(jīng)記錄鎖,而不需添加gap鎖。

          1號紅框的內(nèi)容比較好理解,我們舉個例子看一下2號紅框是在說什么。比方說下邊這個查詢:

          SELCT * FROM hero WHERE number >= 8 FOR UPDATE;

          我們假設(shè)這個語句在隔離級別為REPEATABLE READ。

          很顯然,優(yōu)化器會掃描[8, +∞)的聚簇索引記錄。首先要通過B+樹定位到掃描區(qū)間[8, +∞)的第一條記錄,也就是number值為8的聚簇索引記錄,這條記錄就是掃描區(qū)間[8, +∞)的開始邊界記錄。按理說在REPEATABLE READ隔離級別下應(yīng)該添加next-key鎖,但由于2號紅框中代碼的存在,僅會給number值為8的聚簇索引記錄添加正經(jīng)記錄鎖

          小貼士:

          2號方框的優(yōu)化主要是基于“主鍵值是唯一的”這條約束,在一個事務(wù)執(zhí)行了上述查詢之后,其他事務(wù)是不能插入number值為8的記錄的,這也用不著gap鎖了。

          除了1號方框2號方框的場景,其余場景都給記錄加next-key鎖就好嘍~

          6. 判斷索引條件下推的條件是否成立

          如果是使用二級索引執(zhí)行查詢,并且有索引條件下推(Index Condition Pushdown,簡稱ICP)的條件的話,判斷下推的條件是否成立:

          這里大家特別注意一下,在使用二級索引執(zhí)行查詢,對于非精確匹配的掃描區(qū)間來說,形成掃描區(qū)間的邊界條件也會被當(dāng)作ICP條件下推到存儲引擎判斷,比方說下邊這個查詢:

          mysql> EXPLAIN SELECT * FROM hero WHERE name > 's孫權(quán)' AND name < 'z諸葛亮' FOR UPDATE;+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+| id | select_type | table | partitions | type  | possible_keys | key      | key_len | ref  | rows | filtered | Extra                 |+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+|  1 | SIMPLE      | hero  | NULL       | range | idx_name      | idx_name | 303     | NULL |    1 |   100.00 | Using index condition |+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-----------------------+1?row?in?set,?1?warning?(0.03?sec)

          ?

          可以看到優(yōu)化器決定使用idx_name執(zhí)行上述查詢,對應(yīng)的掃描區(qū)間就是('s孫權(quán)', 'z諸葛亮'),形成這個掃描區(qū)間的邊界條件就是name > 's孫權(quán)' AND name < 'z諸葛亮'。在執(zhí)行計劃的Extra列中出現(xiàn)了Using index condition,表明將邊界條件name > 's孫權(quán)' AND name < 'z諸葛亮'作為ICP條件下推到了存儲引擎。

          不下推不要緊,一下推的話row_search_idx_cond_check就會判斷當(dāng)前記錄是否已經(jīng)不在掃描區(qū)間中了,如果不在掃描區(qū)間中的話,該函數(shù)就會返回ICP_OUT_OF_RANGE。這樣的話,err被賦值為DB_RECORD_NOT_FOUND,這意味著向server層報告當(dāng)前掃描區(qū)間的記錄都已經(jīng)掃描完了,server層在收到這個信息后就會停止向Innodb索要下一條記錄的請求,即結(jié)束本掃描區(qū)間的查詢。

          當(dāng)然,如果本次查詢沒有ICP條件,row_search_idx_cond_check直接返回ICP_MATCH,那就沒有上述的麻煩事兒,繼續(xù)向下走。

          7. 回表對記錄加鎖

          如果row_search_mvcc讀取的是二級索引記錄,則還需進(jìn)行回表,找到相應(yīng)的聚簇索引記錄后需對該聚簇索引記錄加一個正經(jīng)記錄鎖

          其中,row_sel_get_clust_rec_for_mysql便是用于回表的函數(shù),對聚簇索引進(jìn)行加鎖的邏輯在該函數(shù)中實現(xiàn),我們這里就不展開了。

          需要注意的是,即使是對于覆蓋索引的場景下,如果我們想對記錄加X型鎖(也就是使用SELECT ... FOR UPDATE、DELETE、UPDATE語句時)時,也需要對二級索引記錄執(zhí)行回表操作,并給相應(yīng)的聚簇索引記錄添加正經(jīng)記錄鎖

          8. row_search_mvcc返回,判斷是否已經(jīng)到達(dá)邊界

          每當(dāng)處理完一條記錄后,還需要判斷一下這條記錄還在不在掃描區(qū)間中,判斷的代碼如下:

          如果當(dāng)前記錄還在掃描區(qū)間中,就給server層正常返回,如果不在了,就給server層返回一個HA_ERR_END_OF_FILE信息,表示當(dāng)前掃描區(qū)間的記錄都已經(jīng)掃描完了,server層在收到這個信息后就會停止向Innodb索要下一條記錄的請求,即結(jié)束本掃描區(qū)間的查詢。

          小貼士:

          之前在處理精確匹配以及ICP條件時可能把err變量賦值為DB_RECORD_NOT_FOUND,其實后續(xù)代碼會將這種情況也轉(zhuǎn)換為給server層返回HA_ERR_END_OF_FILE信息。

          9. 然后,再處理下一條記錄

          server層收到InnoDB的一條記錄后,如果收到InnoDB通知的本掃描區(qū)間已經(jīng)掃描完畢的信息,則結(jié)束本掃描區(qū)間的查詢;否則繼續(xù)向InnoDB要下一條記錄,也就是需要繼續(xù)執(zhí)行一遍row_search_mvcc函數(shù)了。

          不過此時并不是定位掃描區(qū)間中的第一條記錄,而是根據(jù)記錄所在的鏈表去取下一條記錄即可,所以直接從步驟3開始執(zhí)行就好了,又開始了新的一條記錄的加鎖流程。。。

          循環(huán)往復(fù),直到server層收到本掃描區(qū)間所有記錄都掃描完了的信息為止。

          還有一些釋放鎖的場景

          大家在步驟8判斷當(dāng)前記錄是否已經(jīng)不再掃描區(qū)間中時可以看到,如果當(dāng)前記錄不在掃描區(qū)間中,會執(zhí)行一個unlock_row的函數(shù),這個函數(shù)主要是用于在隔離級別不大于READ COMMITTED時釋放當(dāng)前記錄上的鎖(如果是二級索引記錄還要釋放相應(yīng)的聚簇索引記錄上的鎖)。

          釋放鎖的場景并不是只有這么一個,在row_search_mvcc中也有幾處釋放鎖的場景,我們這里就不多嘮叨了。

          總結(jié)一下

          其實大家再回頭看row_search_mvcc里的關(guān)于加鎖的代碼就會發(fā)現(xiàn),其實流程還是很簡單的:

          ?步驟1. 定位掃描區(qū)間的第一條記錄。

          ?步驟2. 如果掃描區(qū)間是從右到左掃描,那么需要給掃描區(qū)間最右邊的記錄的下一條記錄添加一個gap鎖(在隔離級別不小于REPEATABLE READ并且也沒有開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量的情況下)。

          ?步驟3. 對于Infimum記錄是不加鎖的,對于Supremum記錄next-key鎖(在隔離級別不小于REPEATABLE READ并且也沒有開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量的情況下)。

          ?步驟4. 對于精確匹配的掃描區(qū)間來說,當(dāng)掃描區(qū)間中的記錄都被讀完后,需對掃描區(qū)間后的第一條記錄加一個gap鎖即可,并且向server層返回可結(jié)束本掃描區(qū)間的查詢的信息(在隔離級別不小于REPEATABLE READ并且也沒有開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量的情況下)。

          ?步驟5. 事務(wù)的隔離級別不大于READ COMMITTED,開啟innodb_locks_unsafe_for_binlog系統(tǒng)變量,唯一性搜索并且該記錄的delete_flag不為1,對于>= 主鍵的這種邊界條件來說,當(dāng)前記錄恰好是開始邊界記錄,則對記錄加正經(jīng)記錄鎖,否則添加next-key鎖

          ?步驟6. 判斷ICP條件是否成立。如果當(dāng)前記錄是二級索引記錄,并且已經(jīng)不在掃描區(qū)間中,則向server層返回可結(jié)束本掃描區(qū)間的查詢的信息。

          ?步驟7. 如果對二級索引記錄進(jìn)行加鎖,還需要對相應(yīng)的聚簇索引記錄加正經(jīng)記錄鎖(使用覆蓋索引,并且加S型鎖的記錄可跳過此步驟)。

          ?步驟8. 判斷當(dāng)前記錄是否已不在掃描區(qū)間中,如果不在的話,則向server返回可結(jié)束本掃描區(qū)間的查詢的信息。

          ?步驟9. 如果server層收到可結(jié)束本掃描區(qū)間的查詢的信息,則結(jié)束本掃描區(qū)間的查詢,否則繼續(xù)向InnoDB要下一條記錄,InnoDB根據(jù)記錄所在的鏈表獲取到下一條記錄后,從步驟3開始新一輪的輪回。

          好了,到現(xiàn)在為止大家應(yīng)該明白為什么最開始說的即使是全表掃描的加鎖讀,加的也是行鎖而不是表鎖了。在使用InnoDB存儲引擎時,當(dāng)進(jìn)行全表掃描時,其實就是相當(dāng)于掃描主鍵值在(-∞, +∞)這個掃描區(qū)間中的聚簇索引記錄,針對每一條聚簇索引記錄,都需要執(zhí)行一次row_search_mvcc函數(shù),都需要進(jìn)行如上所述的各種判斷,最后決定給掃描的記錄加什么鎖。


          — 本文結(jié)束 —


          ●?漫談設(shè)計模式在 Spring 框架中的良好實踐

          ●?顛覆微服務(wù)認(rèn)知:深入思考微服務(wù)的七個主流觀點(diǎn)

          ●?人人都是 API 設(shè)計者

          ●?一文講透微服務(wù)下如何保證事務(wù)的一致性

          ●?要黑盒測試微服務(wù)內(nèi)部服務(wù)間調(diào)用,我該如何實現(xiàn)?



          關(guān)注我,回復(fù) 「加群」 加入各種主題討論群。



          對「服務(wù)端思維」有期待,請在文末點(diǎn)個在看

          喜歡這篇文章,歡迎轉(zhuǎn)發(fā)、分享朋友圈


          在看點(diǎn)這里
          瀏覽 28
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  男人的天堂最新资源 | 女人18片毛片60分钟视频 | 色色射| 高清一区二区三区 | 色四月婷婷网五月天 |