本文前提:
代碼mysql 8.0.13
只整理repeatable read
當(dāng)前讀。read committed
簡(jiǎn)單很多,另外快照讀是基于mvcc
不用加鎖,所以不在本文討論范疇。
1. lock 與 latch
innodb
中的lock
是事務(wù)中對(duì)訪問/修改的record
加的鎖,它一般是在事務(wù)提交或回滾時(shí)釋放。latch是在btree上定位record
的時(shí)候?qū)tree pages加的鎖,它一般是在對(duì)page中對(duì)應(yīng)record
加上lock并且完成訪問/修改后就釋放,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ù)起來,確保其他并發(fā)事務(wù)無法修改,所以這時(shí)候沒必要還占著record
所在的page latch
,否則其他事務(wù) 訪問/修改 相同page
的不同record
時(shí),這本來是可以并行做的事情,在這里會(huì)被page latch
會(huì)被卡住。
lock是存在lock_sys->rec_hash
中,每個(gè)record lock
在rec_hash
中通過<space_id
, page_no
, heap_no>
來標(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í)別就不展開說了,這里主要說下rr,從名字上也能看出來,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來保護(hù)起來。在innodb的實(shí)現(xiàn)中,并沒有一個(gè)一下鎖住某個(gè)指定區(qū)間的鎖,而是把一個(gè)大的區(qū)間鎖拆分放在區(qū)間中已有的多個(gè)record上來完成。所以引入了gap lock和next-key lock的概念,它們加再一個(gè)具體的record上
-
gap lock
保護(hù)這個(gè)record與其前一個(gè)record之間的開區(qū)間 -
next-key lock
保護(hù)包含這個(gè)record與其前一個(gè)record之間的左開右閉區(qū)間
它們都是為了保護(hù)這個(gè)區(qū)間不能被別的事務(wù)插入新的record,實(shí)現(xiàn)rr。
接下來從源碼實(shí)現(xiàn)上來分別看下insert和select是如何加lock的,結(jié)合著看也就知道innodb的rr是如何實(shí)現(xiàn)的了。insert的加鎖分布在insert操作的過程中,遍布在多個(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í)際類型,record lock是作用在單個(gè)record上的記錄鎖,gap lock/next-key lock
雖然也是加在某個(gè)具體record上,但作用是為了確保record前面的gap不要有其他并發(fā)事務(wù)插入,這個(gè)具體是怎么實(shí)現(xiàn)呢,innodb引入了一個(gè)插入意向鎖,他的實(shí)際類型是
(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è)
如果不沖突,則代表鎖兼容,無需等待,如果沖突,則接著做gap mode的沖突例外檢測(cè),整理如下:
如果gap mode不沖突,則作為例外情況可以認(rèn)為鎖兼容,無需等待。可以看到:
-
插入意向鎖需要等待
gap lock
及next-key lock
- 任何鎖不用等待插入意向鎖
-
gap lock
無需等待任何鎖 -
next-key lock
需要等待其他next-key lock及record lock
,反之亦然
了解了這些鎖兼容原則,接下來就可以看在實(shí)際insert流程中是如何使用它們的。
3.2 加鎖流程
insert
的順序是先插入主鍵索引,再依次插入二級(jí)索引。以下是從代碼中整理出來的流程,插入某個(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,說明的確有duplicate,返回
db_duplicate_key
到上層,然后上層通過看是insert on duplicate key update
還是普通insert來決定是轉(zhuǎn)成update操作繼續(xù)還是給用戶報(bào)錯(cuò)duplicate -
如果是deleted mark,則說明實(shí)際沒有
duplicate record
,接著往下走
(3)判斷record的下一個(gè)record上當(dāng)前有沒有鎖,如果有的話,則給其加插入意向鎖,確保要插入entry的區(qū)間沒有其他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,說明的確有duplicate,返回
db_duplicate_key
到上層,然后上層通過看是insert on duplicate key update還
是普通insert來決定是轉(zhuǎn)成update操作繼續(xù)還是給用戶報(bào)錯(cuò)duplicate - 如果是delete mark,則實(shí)際沒有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)前有沒有鎖,如果有的話,則給其加插入意向鎖
(lock_x | lock_gap | lock_insert_intention)
確保要插入entry的區(qū)間沒有其他gap lock/next-key lock
保護(hù)
(5)插入entry
(6)釋放page latch
注:【二級(jí)索引】的步驟3似乎有些多余,因?yàn)榧词褂衅渌l(fā)事務(wù)使用insert on duplicate key update
來插入相同record的話,和【主鍵索引】流程一樣,步驟1也只能串行進(jìn)入,第一個(gè)線程沒有找到與entry相同的record,走步驟4插入,直到步驟6結(jié)束釋放page latch之后,第二個(gè)線程才能進(jìn)到步驟1里,此時(shí)在步驟2中會(huì)中卡在加record的x next-key lock
上,直到線程一事務(wù)提交之后才能接著進(jìn)行,所以看起來不會(huì)沖突?
上述流程在row_ins_index_entry函數(shù)中,具體入口如下:
1
2
3
4
|
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ù)里加的,入口如下:
1
2
3
|
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è)隱式鎖,它是通過record的隱藏字段trx_id來檢測(cè)的,對(duì)于主鍵索引,如果要插入的record在btree中找到,那么只需要通過比較已有record的trx_id,如果這個(gè)trx_id對(duì)應(yīng)的事務(wù)還是活躍事務(wù),那么說明這個(gè)record的插入事務(wù)還未提交,隱式代表這個(gè)record上有鎖,那么此時(shí)就才會(huì)將其轉(zhuǎn)成顯式鎖放進(jìn)lock_sys
中并wait,這樣做是為了提高性能,盡量減少對(duì)lock_sys的操作。對(duì)于二級(jí)索引的隱式鎖檢測(cè)就沒有主鍵索引這么容易了,因?yàn)槎?jí)索引record沒有記錄trx_id
,只能首先通過其所在page上的max_trx_id
與當(dāng)前活躍事務(wù)列表的最小trx_id來比較,小于它的話代表最后一次修改這個(gè)page的事務(wù)都已經(jīng)提交,所以record上沒有隱式鎖,如果大于或等于它的話,就需要回主鍵找到對(duì)應(yīng)的主鍵record并遍歷undo歷史版本來確認(rèn)是否有隱式鎖,具體實(shí)現(xiàn)在row_vers_impl_x_locked_low
中,
4. select 加鎖流程
select做當(dāng)前讀的加鎖流程就在row_search_mvcc當(dāng)中,一條select語句會(huì)多次進(jìn)入這個(gè)函數(shù),第一次是通過index_read->row_search_mvcc
進(jìn)來,一般是首次訪問index,取找where里的exact record,之后每次再通過general_fetch->row_search_mvcc
進(jìn)來,根據(jù)具體條件遍歷prev/next record
,直到把滿足whrer條件的record都取出來。具體的加鎖也就是在訪問和遍歷record的過程中進(jìn)行,row_search_mvcc
代碼很長(zhǎng),這里我只提煉總結(jié)下加鎖相關(guān)的流程:
-
在index上查找search_tuple對(duì)應(yīng)的record。(這里的record可能是上面說的index_read進(jìn)來首次通過index btree查找
search_tuple
對(duì)應(yīng)的record,也有可能是之后多次general_fetch進(jìn)來通過之前保存的cursor來恢復(fù)出來的上一次訪問位置,然后拿到的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 - 到這里說明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
- 到這里說明加鎖成功了,然后處理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來取對(duì)應(yīng)的
prev/next record
,跳轉(zhuǎn) 步驟3 繼續(xù)
重點(diǎn)說一下步驟3,這里一般record是infimum或者supremum的情況都是多次genera_fetch對(duì)某個(gè)page取prev/next record之后走到page邊緣,對(duì)于infimum,不會(huì)加任何lock,直接繼續(xù)訪問前一個(gè)prev record(即prev page的supremum),對(duì)于supremum的話,會(huì)加上gap lock,它保護(hù)當(dāng)前page最后一個(gè)user record和next page第一個(gè)user record之間的gap。
其他的流程也就沒什么了:
- 對(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)基本也就出來了。
到此這篇關(guān)于mysql innodb 事務(wù)鎖源碼分析的文章就介紹到這了,更多相關(guān)mysql innodb 事務(wù)鎖源碼分析內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:http://kernelmaker.github.io/MySQL_Lock?utm_source=tuicool&utm_medium=referral