MySQL InnoDB 事務(wù)鎖源碼分析
本文前提:
代碼MySQL 8.0.13
只整理Repeatable Read
當(dāng)前讀。Read Committed
簡(jiǎn)單很多,另外快照讀是基于MVCC
不用加鎖,所以不在本文討論范疇。
1. Lock 與 Latch
InnoDB
中的lock
是事務(wù)中對(duì)訪問(wèn)/修改的record
加的鎖,它一般是在事務(wù)提交或回滾時(shí)釋放。latch是在BTree上定位record
的時(shí)候?qū)tree pages加的鎖,它一般是在對(duì)page中對(duì)應(yīng)record
加上lock并且完成訪問(wèn)/修改后就釋放,latch的鎖區(qū)間比lock小很多。在具體的實(shí)現(xiàn)中,一個(gè)大的transaction
會(huì)被拆成若干小的mini transaction(mtr
),如下圖所示:有一個(gè)transaction
,依次做了insert
,select…for update
及update
操作,這3個(gè)操作分別對(duì)應(yīng)3個(gè)mtr,每個(gè)mtr完成:
- 在btree查找目標(biāo)
record
,加相關(guān)page latch
; - 加目標(biāo)
record lock
,修改對(duì)應(yīng)record
- 釋放
page latch
為什么要這么做呢?是為了并發(fā),事務(wù)中的每一個(gè)操作,在步驟二完成之后,相應(yīng)的record
已經(jīng)加上了lock保護(hù)起來(lái),確保其他并發(fā)事務(wù)無(wú)法修改,所以這時(shí)候沒(méi)必要還占著record
所在的page latch
,否則其他事務(wù) 訪問(wèn)/修改 相同page
的不同record
時(shí),這本來(lái)是可以并行做的事情,在這里會(huì)被page latch
會(huì)被卡住。
lock是存在lock_sys->rec_hash
中,每個(gè)record lock
在rec_hash
中通過(guò)<space_id
, page_no
, heap_no>
來(lái)標(biāo)識(shí)
latch
是存在bufferpool
對(duì)應(yīng)page
的block
中,對(duì)應(yīng)block->lock
本文只關(guān)注lock相關(guān)的東西,latch后面單獨(dú)搞一篇整理
2. Repeatable Read
具體每個(gè)隔離級(jí)別就不展開(kāi)說(shuō)了,這里主要說(shuō)下RR,從名字上也能看出來(lái),RR支持可重復(fù)度,也就是在一個(gè)事務(wù)中,多次執(zhí)行相同的SELECT…FOR UPDATE
應(yīng)該看到相同的結(jié)果集(除本事務(wù)修改外),這個(gè)就要求SELECT的區(qū)間里不能有其他事務(wù)插入新的record,所以SELECT除了對(duì)滿足條件的record加lock之外,對(duì)相應(yīng)區(qū)間也要加lock來(lái)保護(hù)起來(lái)。在InnoDB的實(shí)現(xiàn)中,并沒(méi)有一個(gè)一下鎖住某個(gè)指定區(qū)間的鎖,而是把一個(gè)大的區(qū)間鎖拆分放在區(qū)間中已有的多個(gè)record上來(lái)完成。所以引入了Gap lock和Next-key lock的概念,它們加再一個(gè)具體的record上
Gap lock
保護(hù)這個(gè)record與其前一個(gè)record之間的開(kāi)區(qū)間Next-key lock
保護(hù)包含這個(gè)record與其前一個(gè)record之間的左開(kāi)右閉區(qū)間
它們都是為了保護(hù)這個(gè)區(qū)間不能被別的事務(wù)插入新的record,實(shí)現(xiàn)RR。
接下來(lái)從源碼實(shí)現(xiàn)上來(lái)分別看下Insert和Select是如何加lock的,結(jié)合著看也就知道InnoDB的RR是如何實(shí)現(xiàn)的了。Insert的加鎖分布在Insert操作的過(guò)程中,遍布在多個(gè)相關(guān)的函數(shù)里,Select的加鎖則比較集中,就在row_search_mvcc
里。
3. Insert加鎖流程
3.1 lock mode
lock的mode主要有Share(S)和Exclusive(X)【代碼中對(duì)應(yīng)LOCK_S和LOCK_X】
lock的gap mode主要有Record lock, Gap lock, Next-key lock【代碼中對(duì)應(yīng)LOCK_REC_NOT_GAP, LOCK_GAP, LOCK_ORDINARY】
在具體使用中將 mode|gap_mode 之后就是一個(gè)lock的實(shí)際類(lèi)型,Record lock是作用在單個(gè)record上的記錄鎖,Gap lock/Next-key lock
雖然也是加在某個(gè)具體record上,但作用是為了確保record前面的gap不要有其他并發(fā)事務(wù)插入,這個(gè)具體是怎么實(shí)現(xiàn)呢,InnoDB引入了一個(gè)插入意向鎖,他的實(shí)際類(lèi)型是
(LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION)
與Gap lock/Next-key lock
互斥,如果要插入前檢測(cè)到插入位置的next record上有l(wèi)ock,則會(huì)嘗試對(duì)這個(gè)next record加一個(gè)插入意向鎖,代表本事務(wù)打算給這個(gè)gap里插一個(gè)新record,看行不行?如果已經(jīng)有別的事務(wù)給這里上了Gap/Next-key lock
,代表它想保護(hù)這里,所以當(dāng)前插入意向鎖需要等待相關(guān)事務(wù)提交才行。這個(gè)檢測(cè)只是單向的,即插入意向鎖需等待Gap/Next-key lock
釋放,而任何鎖不用等待插入意向鎖釋放,否則嚴(yán)重影響這個(gè)gap中不沖突的Insert操作并發(fā)。
具體的鎖沖突檢測(cè)在lock_rec_has_to_wait函數(shù)中,大體原則就是:判斷兩個(gè)lock兼容還是不兼容,首先先做mode的沖突檢測(cè)
如果不沖突,則代表鎖兼容,無(wú)需等待,如果沖突,則接著做gap mode的沖突例外檢測(cè),整理如下:
如果gap mode不沖突,則作為例外情況可以認(rèn)為鎖兼容,無(wú)需等待。可以看到:
- 插入意向鎖需要等待
Gap lock
及Next-key lock
- 任何鎖不用等待插入意向鎖
Gap lock
無(wú)需等待任何鎖Next-key lock
需要等待其他Next-key lock及Record Lock
,反之亦然
了解了這些鎖兼容原則,接下來(lái)就可以看在實(shí)際Insert流程中是如何使用它們的。
3.2 加鎖流程
Insert
的順序是先插入主鍵索引,再依次插入二級(jí)索引。以下是從代碼中整理出來(lái)的流程,插入某個(gè)entry
的操作,
【對(duì)于主鍵索引】:
(1)先在查找Btree,加相關(guān)page latch
,定位到entry對(duì)應(yīng)插入位置的record (<= entry)
(2)如果要插入的entry已經(jīng)存在,即entry = record
,此時(shí)接著判斷:
- 如果是
INSERT ON DUPLICATE KEY UPDATE
,則對(duì)record
加X Next-key lock
- 如果是普通
INSERT
,則對(duì)record
加S Next-key lock
之后接著判斷record是否是deleted mark:
- 如果不是delete mark,說(shuō)明的確有duplicate,返回
DB_DUPLICATE_KEY
到上層,然后上層通過(guò)看是INSERT ON DUPLICATE KEY UPDATE
還是普通INSERT來(lái)決定是轉(zhuǎn)成update操作繼續(xù)還是給用戶(hù)報(bào)錯(cuò)duplicate - 如果是deleted mark,則說(shuō)明實(shí)際沒(méi)有
duplicate record
,接著往下走
(3)判斷record的下一個(gè)record上當(dāng)前有沒(méi)有鎖,如果有的話,則給其加插入意向鎖,確保要插入entry的區(qū)間沒(méi)有其他Gap lock/Next-key lock
保護(hù)
(4)插入entry
(5)釋放page latch
,此時(shí)依舊占有l(wèi)ock
【對(duì)于二級(jí)索引】
(1)先在查找Btree,加相關(guān)page latch,定位到entry對(duì)應(yīng)插入位置的record (<= entry)
(2)如果要插入的entry已經(jīng)存在,即entry = record
,并且當(dāng)前index是unique:
- 如果是
INSERT ON DUPLICATE KEY UPDATE
,則對(duì)record
加X Next-key lock
- 如果是普通INSERT,則對(duì)
record2
加S Next-key lock
判斷record與entry是否相等:
如果相等 并且 是普通INSERT,則接著判斷record是否是deleted mark:
- 如果不是delete mark,說(shuō)明的確有duplicate,返回
DB_DUPLICATE_KEY
到上層,然后上層通過(guò)看是INSERT ON DUPLICATE KEY UPDATE還
是普通INSERT來(lái)決定是轉(zhuǎn)成update操作繼續(xù)還是給用戶(hù)報(bào)錯(cuò)duplicate - 如果是delete mark,則實(shí)際沒(méi)有duplicate,接著往下走
(3)如果是INSERT ON DUPLICATE KEY UPDATE
并且 當(dāng)前index是unique,則給其下一個(gè)record X Gap lock
,保護(hù)不會(huì)被其他事務(wù)插入相同的entry
(4)判斷record的下一個(gè)record上當(dāng)前有沒(méi)有鎖,如果有的話,則給其加插入意向鎖
(LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION)
確保要插入entry的區(qū)間沒(méi)有其他Gap lock/Next-key lock
保護(hù)
(5)插入entry
(6)釋放page latch
注:【二級(jí)索引】的步驟3似乎有些多余,因?yàn)榧词褂衅渌l(fā)事務(wù)使用INSERT ON DUPLICATE KEY UPDATE
來(lái)插入相同record的話,和【主鍵索引】流程一樣,步驟1也只能串行進(jìn)入,第一個(gè)線程沒(méi)有找到與entry相同的record,走步驟4插入,直到步驟6結(jié)束釋放page latch之后,第二個(gè)線程才能進(jìn)到步驟1里,此時(shí)在步驟2中會(huì)中卡在加record的X Next-key lock
上,直到線程一事務(wù)提交之后才能接著進(jìn)行,所以看起來(lái)不會(huì)沖突?
上述流程在row_ins_index_entry函數(shù)中,具體入口如下:
mysql_parse->mysql_execute_command->Sql_cmd_dml::execute-> Sql_cmd_insert_values::execute_inner->write_record->handler::ha_write_row-> ha_innobase::write_row->row_insert_for_mysql->row_insert_for_mysql_using_ins_graph-> row_ins_step->row_ins->row_ins_index_entry_step->row_ins_index_entry
其中插入意向鎖是在lock_rec_insert_check_and_lock函數(shù)里加的,入口如下:
row_ins_index_entry->row_ins_clust_index_entry/row_ins_sec_index_entry-> btr_cur_optimistic_insert/btr_cur_pessimistic_insert->btr_cur_ins_lock_and_undo-> lock_rec_insert_check_and_lock
3.3 隱式鎖
另外要提的一點(diǎn)就是,Insert操作不會(huì)顯式的加鎖,每一條Insert的record上都默認(rèn)有一個(gè)隱式鎖,它是通過(guò)record的隱藏字段trx_id來(lái)檢測(cè)的,對(duì)于主鍵索引,如果要插入的record在Btree中找到,那么只需要通過(guò)比較已有record的trx_id,如果這個(gè)trx_id對(duì)應(yīng)的事務(wù)還是活躍事務(wù),那么說(shuō)明這個(gè)record的插入事務(wù)還未提交,隱式代表這個(gè)record上有鎖,那么此時(shí)就才會(huì)將其轉(zhuǎn)成顯式鎖放進(jìn)lock_sys
中并wait,這樣做是為了提高性能,盡量減少對(duì)lock_sys的操作。對(duì)于二級(jí)索引的隱式鎖檢測(cè)就沒(méi)有主鍵索引這么容易了,因?yàn)槎?jí)索引record沒(méi)有記錄trx_id
,只能首先通過(guò)其所在page上的max_trx_id
與當(dāng)前活躍事務(wù)列表的最小trx_id來(lái)比較,小于它的話代表最后一次修改這個(gè)page的事務(wù)都已經(jīng)提交,所以record上沒(méi)有隱式鎖,如果大于或等于它的話,就需要回主鍵找到對(duì)應(yīng)的主鍵record并遍歷undo歷史版本來(lái)確認(rèn)是否有隱式鎖,具體實(shí)現(xiàn)在row_vers_impl_x_locked_low
中,
4. Select 加鎖流程
SELECT做當(dāng)前讀的加鎖流程就在row_search_mvcc當(dāng)中,一條SELECT語(yǔ)句會(huì)多次進(jìn)入這個(gè)函數(shù),第一次是通過(guò)index_read->row_search_mvcc
進(jìn)來(lái),一般是首次訪問(wèn)index,取找WHERE里的exact record,之后每次再通過(guò)general_fetch->row_search_mvcc
進(jìn)來(lái),根據(jù)具體條件遍歷prev/next record
,直到把滿足WHRER條件的record都取出來(lái)。具體的加鎖也就是在訪問(wèn)和遍歷record的過(guò)程中進(jìn)行,row_search_mvcc
代碼很長(zhǎng),這里我只提煉總結(jié)下加鎖相關(guān)的流程:
- 在index上查找search_tuple對(duì)應(yīng)的record。(這里的record可能是上面說(shuō)的index_read進(jìn)來(lái)首次通過(guò)index Btree查找
search_tuple
對(duì)應(yīng)的record,也有可能是之后多次general_fetch進(jìn)來(lái)通過(guò)之前保存的cursor來(lái)恢復(fù)出來(lái)的上一次訪問(wèn)位置,然后拿到的prev/next record) - 如果是index_read 并且 mode是
PAGE_CUR_L
或著PAGE_CUR_LE
,給定位到的record的next record加 GAP LOCK - 如果record是infimum,跳轉(zhuǎn)步驟9 next_rec,如果是supremum,加Next-key Lock,跳轉(zhuǎn)步驟9 next_rec
- 如果是index_read,record與search_tuple不相等,給
record
加GAP LOCK
,返回 NOT FOUND - 到這里說(shuō)明record與search_tuple相等,給record加Next-key Lock,兩個(gè)例外,只加Rec Lock:
- 對(duì)于index_read,如果當(dāng)前index是主鍵索引 并且
mode
是PAGE_CUR_GE
并且search_tuple
的fields個(gè)數(shù)等于index的unique fields個(gè)數(shù) - 看是否是unique_search,即search_tuple的fields個(gè)數(shù)等于當(dāng)前index的unique fields個(gè)數(shù) 并且 當(dāng)前index是主鍵索引或者(是二級(jí)索引且search_tuple不包含NULL字段)并且 record不是deleted mark
- 到這里說(shuō)明加鎖成功了,然后處理record是deleted mark的情況:
- 當(dāng)前index是主鍵索引 并且 是
unique_search
,返回NOT FOUND
- 否則,跳轉(zhuǎn)步驟9 next_rec
- 如果當(dāng)前index是二級(jí)索引 并且 需要回查主鍵索引,去主鍵索引里找對(duì)應(yīng)的
primary record
并加Rec Lock
,如果primary record是deleted mark,則當(dāng)前二級(jí)索引接著跳轉(zhuǎn)步驟9 next_rec - 成功,返回
DB_SUCCESS
- next_rec: 根據(jù)mode來(lái)取對(duì)應(yīng)的
prev/next record
,跳轉(zhuǎn) 步驟3 繼續(xù)
重點(diǎn)說(shuō)一下步驟3,這里一般record是infimum或者supremum的情況都是多次genera_fetch對(duì)某個(gè)page取prev/next record之后走到page邊緣,對(duì)于infimum,不會(huì)加任何lock,直接繼續(xù)訪問(wèn)前一個(gè)prev record(即prev page的supremum),對(duì)于supremum的話,會(huì)加上Gap lock,它保護(hù)當(dāng)前page最后一個(gè)user record和next page第一個(gè)user record之間的Gap。
其他的流程也就沒(méi)什么了:
- 對(duì)于遍歷到的滿足條件的record,基本默認(rèn)都是加Next-key lock
- 二級(jí)索引回表時(shí)只會(huì)對(duì)主鍵加Rec lock
- 對(duì)于某些特殊的場(chǎng)景,會(huì)將某些
Next-key lock
降級(jí)成Rec lock
(步驟5) - 還有一些特殊場(chǎng)景,會(huì)只加Gap lock(步驟2、4)
總結(jié):
以上基本就是InnoDB
加事務(wù)鎖的相關(guān)流程,Insert
和Select
的加鎖流程配合著看,事務(wù)鎖的原則及實(shí)現(xiàn)基本也就出來(lái)了。
到此這篇關(guān)于MySQL InnoDB 事務(wù)鎖源碼分析的文章就介紹到這了,更多相關(guān)MySQL InnoDB 事務(wù)鎖源碼分析內(nèi)容請(qǐng)搜索本站以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持本站!
版權(quán)聲明:本站文章來(lái)源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來(lái)源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來(lái)源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來(lái),僅供學(xué)習(xí)參考,不代表本站立場(chǎng),如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。