MySQL数据库InnoDB存储引擎中的锁机制--转载
原文地址:http://www.uml.org.cn/sjjm/201205302.asp
00 – 基本概念
當(dāng)并發(fā)事務(wù)同時(shí)訪問一個(gè)資源的時(shí)候,有可能導(dǎo)致數(shù)據(jù)不一致。因此需要一種致機(jī)制來將訪問順序化。
鎖就是其中的一種機(jī)制。我們用商場的試衣間來做一個(gè)比喻。試衣間供許多消費(fèi)者使用。因此可能有多個(gè)消費(fèi)者同時(shí)要試衣服。為了避免沖突,試衣間的門上裝了鎖。試衣服的人在里邊鎖住,其他人就不能從外邊打開了。只有里邊的人開門出來,外邊的人才能進(jìn)去。
- 鎖的基本類型
數(shù)據(jù)庫上的操作可以歸納為兩中,讀和寫。多個(gè)事務(wù)同時(shí)讀一個(gè)對象的時(shí)候,是不會(huì)有沖突的。
同時(shí)讀和寫或者同時(shí)寫才會(huì)產(chǎn)生沖突。因此為了提高并發(fā)性,通常定義兩種鎖:
A. 共享鎖(Shared Lock) 也叫讀鎖.
共享鎖表示對數(shù)據(jù)進(jìn)行讀操作。因此多個(gè)事務(wù)可以同時(shí)為一個(gè)對象加共享鎖。
B. 排他鎖(Exclusive Lock) 也叫寫鎖.
排他鎖表示對數(shù)據(jù)進(jìn)行寫操作。如果一個(gè)事務(wù)對對象加了排他鎖,其他事務(wù)就不能再給它加任何鎖了。
- S、X鎖的兼容性矩陣
對于鎖,通常會(huì)用一個(gè)矩陣來描述他們之間的沖突關(guān)系。
S X
S + –
X - -
+ 代表兼容, -代表不兼容
- 鎖的粒度
A. 表鎖(Table Lock)
對整個(gè)表加鎖,影響標(biāo)準(zhǔn)的所有記錄。通常用在DDL語句中,如DELETE TABLE,ALTER TABLE等。
B. 行鎖(Row Lock)
對一行記錄加鎖,只影響一條記錄。通常用在DML語句中,如INSERT, UPDATE, DELETE等。
很明顯,表鎖影響整個(gè)表的數(shù)據(jù),因此并發(fā)性不如行鎖好。
- 意向鎖(Intention Lock)
因?yàn)楸礞i覆蓋了行鎖的數(shù)據(jù),所以表鎖和行鎖也會(huì)產(chǎn)生沖突。如:
A. trx1 BEGI
B. trx1 給 T1 加X鎖,修改表結(jié)構(gòu)。
C. trx2 BEGIN
D. trx2 給 T1 的一行記錄加S或X鎖(事務(wù)被阻塞,等待加鎖成功)。
trx1要操作整個(gè)表,鎖住了整個(gè)表。那么trx2就不能再對T1的單條記錄加X或S鎖,去讀取或修這條記錄。
為了方便檢測表級鎖和行級鎖之間的沖突,就引入了意向鎖。
A. 意向鎖分為意向讀鎖(IS)和意向?qū)戞i(IX)。
B. 意向鎖是表級鎖,但是卻表示事務(wù)正在讀或?qū)懩骋恍杏涗?#xff0c;而不是整個(gè)表。
所以意向鎖之間不會(huì)產(chǎn)生沖突,真正的沖突在加行鎖時(shí)檢查。
C. 在給一行記錄加鎖前,首先要給該表加意向鎖。也就是要同時(shí)加表意向鎖和行鎖。
采用了意向鎖后,上面的例子就變成了:
A. trx1 BEGIN
B. trx1 給 T1 加X鎖,修改表結(jié)構(gòu)。
C. trx2 BEGIN
D. trx2 給 T1 加IX鎖(事務(wù)被阻塞,等待加鎖成功)
E. trx2 給 T1 的一行記錄加S或X鎖.
- 表鎖的兼容性矩陣
IS IX S X
IS + + + –
IX + + - -
S + - + -
X - - - -
+ 代表兼容, -代表不兼容
A. 意向鎖之間不會(huì)沖突, 因?yàn)橐庀蜴i僅僅代表要對某行記錄進(jìn)行操作。在加行鎖時(shí),會(huì)判斷是否沖突。
01 – 行鎖
直觀的理解,行鎖就是要鎖住一行記錄,阻止其他事務(wù)操作該行記錄。這里有一個(gè)隱含的邏輯:
A. 插入操作永遠(yuǎn)不會(huì)被阻止,因?yàn)椴迦氩僮鞑粫?huì)操作一條存在的記錄(這里不考慮Insert duplicate的處理)。這個(gè)邏輯是對的嗎? 這和用戶的使用情況相關(guān),有些情況下是用戶能接受的,有些情況下是用戶不能接受的。
- 幻讀(Phantom Read)
如果不阻止INSERT操作,就會(huì)產(chǎn)生幻讀.MySQL手冊中有幻讀的介紹.
A. MVCC 可以避免幻讀.但是MVCC只對SELECT語句有效,對于SELECT … [LOCK IN SHARE MODE | FOR UPDATE], UPDATE, DELETE語句無效。
B. 為了能夠通過鎖避免幻讀,采用了next-key的機(jī)制。next-key通過鎖住2個(gè)記錄之間的間隙,來阻止INSERT操作。
- 行鎖的模式
行鎖S、X鎖上做了一些精確的細(xì)分,在代碼中稱作Precise Mode。這些精確的模式,使的鎖的粒度更細(xì)小。可以減少?zèng)_突。
A. 間隙鎖(Gap Lock),只鎖間隙。
B. 記錄鎖(Record Lock) 只鎖記錄。
C. Next-Key Lock(代碼中稱為Ordinary Lock),同時(shí)鎖住記錄和間隙.
D. 插入意圖鎖(Insert Intention Lock),插入時(shí)使用的鎖。在代碼中,插入意圖鎖,
實(shí)際上是GAP鎖上加了一個(gè)LOCK_INSERT_INTENTION的標(biāo)記.
MySQL手冊對這些模式有詳細(xì)的介紹.
- 行鎖模式的兼容性矩陣
G I R N (已經(jīng)存在的鎖,包括等待的鎖)
G + + + +
I - + + -
R + + - -
N + + - -
+ 代表兼容, -代表不兼容. I代表插入意圖鎖,
G代表Gap鎖,I代表插入意圖鎖,R代表記錄鎖,N代表Next-Key鎖.
S鎖和S鎖是完全兼容的,因此在判別兼容性時(shí)不需要對比精確模式。
精確模式的檢測,用在S、X和X、X之間。
這個(gè)矩陣是從lock0lock.c:lock_rec_has_to_wait()的代碼推出來的。從這個(gè)矩陣可以看到幾個(gè)特點(diǎn):
A. INSERT操作之間不會(huì)有沖突。
B. GAP,Next-Key會(huì)阻止Insert。
C. GAP和Record,Next-Key不會(huì)沖突
D. Record和Record、Next-Key之間相互沖突。
E. 已有的Insert鎖不阻止任何準(zhǔn)備加的鎖。
同時(shí)也有幾個(gè)疑問:
A. 為什么插入意圖鎖不阻止間隙鎖?在特定的情況下會(huì)導(dǎo)致INSERT操作被無限期延遲。
B. 如果不阻止任何鎖,這個(gè)鎖還有必要存在嗎?
- 目前看到的作用是,通過加鎖的方式來喚醒等待線程。
- 但這并不意味著,被喚醒后可以直接做插入操作了。需要再次判斷是否有鎖沖突。
C. GAP+LOCK_INSERT_INTENTION標(biāo)記的方式,能否直接變成INSERT_INTENTION鎖?
目前還在看。
- B+Tree 行鎖
InnoDB的行鎖并不是簡單的數(shù)據(jù)行鎖的概念。而是指每個(gè)B+Tree上的行鎖,也可以理解為每個(gè)Index上的行鎖。因此操作一行記錄時(shí),有可能會(huì)加多個(gè)行鎖在不同的B+Tree上。如:
CREATE TABLE t1(c1 INT KEY, c2 int, c3 int, INDEX(c2));
INSERT INTO t1 VALUES(1, 1, 1), (3, 3, 3)
UPDATE t1 c3 = 10 WHERE c2 <= 2
UPDATE語句會(huì)同時(shí)在Secondary Index和Clustered Index上加鎖。
- 行鎖模式的使用
行鎖的這些模式都在什么情況下使用呢? MySQL手冊有詳細(xì)的介紹。
A. Next-Key 使用在被WHERE條件用到的索引上(準(zhǔn)確的說是用來做Search的索引上)。
上面的例子中,Index(c2)上使用 Next-Key Lock.
B. Record Lock使用在沒有被WHERE條件使用的索引上。上面的例子中,簇索引上使用Record Lock.因此上面的UPDATE語句會(huì)同時(shí)在加Index(c2)的鍵1上加Next-Key,在主鍵1上加record鎖。當(dāng)另一個(gè)session并發(fā)插入(2,5,2),(3,5,2)時(shí)可以成功,但是(2,2,2)時(shí)會(huì)被阻塞。
Next-Key And Record
測試時(shí)發(fā)現(xiàn),SELECT…[FOR UPDATE |LOCKIN SHARE MODE]可能會(huì)導(dǎo)致全部記錄被鎖住。
當(dāng)表很小時(shí),SELECT會(huì)采用全表掃描的方法。在使用這種方法時(shí),遍歷了所有的數(shù)據(jù),因此所有數(shù)據(jù)都被鎖住了。盡管對不符合條件的記錄調(diào)用了ha_innobase::unlock_row(),但是在Repeatable Read級別時(shí)不會(huì)被釋放。也許該算一個(gè)Bug.
C. A、B同時(shí)適用于SELECT…[FOR UPDATE | LOCK IN SHARE MODE], UPDATE、DELETE語句。
D. GAP鎖顯然也是使用在WHERE條件使用的索引上。和Next-Key不同的是,GAP鎖只加在上邊界(第一個(gè)大于符合條件的記錄)上。而Next-Key加在所有符合條件的記錄上。上面例子中的條件c2=2的記錄,需要在c2=3上加一個(gè)GAP鎖。
? 正向查詢時(shí),InnoDB中實(shí)際上在邊界上加的是Next-Key鎖。 這可能是受實(shí)現(xiàn)的限制。
目前使用GAP情況有:
– Supremum記錄上始終是一個(gè)GAP鎖
– 反向查詢(ORDER BY DESC)時(shí).
– 等值匹配一個(gè)確切的鍵值時(shí),對下一條記錄加GAP鎖。
– 等值匹配一個(gè)確切的鍵值的前綴時(shí),對下一條記錄加GAP鎖。。
E. INSERT時(shí),通常不加鎖。只有當(dāng)其他事務(wù)在插入點(diǎn)加了Gap或Next-key鎖需要等待時(shí),才會(huì)創(chuàng)建一個(gè)插入意圖鎖。這個(gè)鎖是在waiting狀態(tài)。
- 隔離級別對Next-Key鎖的影響
A. Read Uncommitted和Read Committed時(shí),不需要在間隙上加鎖,Nexk-Key變成Record鎖。
B. Repeatable Reads 和 Serializable時(shí),通常情況下使用Next-key鎖。
有2中情況不需要對間隙加鎖:
– 查詢一個(gè)唯一的值,如 WHERE c1 = 1, c1 是主鍵或唯一鍵,并且查詢結(jié)果中不含NULL字段。
– 當(dāng)innodb_locks_unsafe_for_binlog被開啟。這里還是有一些值得思考的問題:
- 從這個(gè)情況來看,UPDATE,DELETE時(shí)加間隙鎖完全是為了防止Master和Slave數(shù)據(jù)不一致。那么不使用binlog時(shí)就沒有必要對DELETE, UPDATE加間隙鎖。
- Row Format Binlog時(shí),不加間隙鎖是否會(huì)引起Master, Slave不一至。
- 即便設(shè)置了innodb_locks_unsafe_for_binlog,SELECT…[]是否可以不加間隙鎖。
判斷加什么鎖的主要工作在row0sel.c:row_search_for_mysql()中。
02 – 延遲加鎖機(jī)制
如果一個(gè)表有很多的索引,那么操作一個(gè)記錄時(shí),豈不是要加很多鎖到不同的B-Tree上嗎?
先來看一個(gè)事務(wù)的狀態(tài)信息:
CREATE TABLE t1(c1 INT KEY, c2 INT);
BEGIN;
INSERT INTO t1 VALUES(1, 1);
INSERT INTO t1 VALUES(2, 2);
SHOW ENGINE INNODB STATUS;
狀態(tài)信息:
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 501, ACTIVE 0 sec
1 lock struct(s), heap size 376, 0 row lock(s), undo log entries 2
– 隱式鎖
Lock 是一種悲觀的順序化機(jī)制。它假設(shè)很可能發(fā)生沖突,因此在操作數(shù)據(jù)時(shí),就加鎖。
如果沖突的可能性很小,多數(shù)的鎖都是不必要的。
Innodb 實(shí)現(xiàn)了一個(gè)延遲加鎖的機(jī)制,來減少加鎖的數(shù)量,在代碼中稱為隱式鎖(Implicit Lock)。
隱式鎖中有個(gè)重要的元素,事務(wù)ID(trx_id).隱式鎖的邏輯過程如下:
A. InnoDB的每條記錄中都一個(gè)隱含的trx_id字段,這個(gè)字段存在于簇索引的B+Tree中。
B. 在操作一條記錄前,首先根據(jù)記錄中的trx_id檢查該事務(wù)是否是活動(dòng)的事務(wù)(未提交或回滾).
如果是活動(dòng)的事務(wù),首先將隱式鎖轉(zhuǎn)換為顯式鎖(就是為該事務(wù)添加一個(gè)鎖)。
C. 檢查是否有鎖沖突,如果有沖突,創(chuàng)建鎖,并設(shè)置為waiting狀態(tài)。如果沒有沖突不加鎖,跳到E。
D. 等待加鎖成功,被喚醒,或者超時(shí)。
E. 寫數(shù)據(jù),并將自己的trx_id寫入trx_id字段。Page Lock可以保證操作的正確性。
相關(guān)代碼:
A. lock_rec_convert_impl_to_expl()將隱式鎖轉(zhuǎn)換成顯示鎖。
B. 加鎖和測試行鎖沖突都用lock_rec_lock(),它的第一個(gè)參數(shù)表示是否是隱式鎖。所以要特別注意這個(gè)參數(shù)。如果為TRUE,在沒有沖突時(shí)并不會(huì)加鎖。
C. 測試行鎖的沖突的具體內(nèi)容在lock_rec_has_wait()
D. 創(chuàng)建waiting鎖是lock_rec_enqueue_waiting()
E. 創(chuàng)建行鎖是lock_rec_add_to_queue()
– 隱式鎖的特點(diǎn)
A. 只有在很可能發(fā)生沖突時(shí)才加鎖,減少了鎖的數(shù)量。
B. 隱式鎖是針對被修改的B+Tree記錄,因此都是Record類型的鎖。不可能是Gap或Next-Key類型。
– 隱式鎖的使用
A. INSERT操作只加隱式鎖,不需要顯示加鎖。
B. UPDATE,DELETE在查詢時(shí),直接對查詢用的Index和主鍵使用顯示鎖,其他索引上使用隱式鎖。
理論上說,可以對主鍵使用隱式鎖的。提前使用顯示鎖應(yīng)該是為了減少死鎖的可能性。
INSERT,UPDATE,DELETE對B+Tree們的操作都是從主鍵的B+Tree開始,因此對主鍵加鎖可以有效的阻止死鎖。
– Secondary Index上的隱式鎖
前邊說了, trx_id只存在于主鍵上,那么輔助索引上如何來實(shí)現(xiàn)隱式索引呢?
顯然是要通過輔助索引中的主鍵值,在主鍵B+Tree上進(jìn)行二次查找。這個(gè)開銷是很大的。
InnoDB對這個(gè)過程有一個(gè)優(yōu)化:
A. 每個(gè)頁上有一個(gè)MAX_TRX_ID,每次修改輔助索引的記錄時(shí),都會(huì)更新這個(gè)最大事務(wù)ID。
B. 當(dāng)判斷是否要將隱式鎖變?yōu)轱@式鎖時(shí),先將頁面的max_trx_id和事務(wù)列表的最小trx_id比較。如果max_trx_id比事務(wù)列表的最小trx_id還小,那么就不需要轉(zhuǎn)換為顯示鎖了。
代碼在lock_sec_rec_some_has_impl_off_kernel()中
/* Some transaction may have an implicit x-lock on the record onlyif the max trx id for the page >= min trx id for the trx list, ordatabase recovery is running. We do not write the changes of a page max trx id to the log, and therefore during recovery, this value for a page may be incorrect. */
if (page_get_max_trx_id(page) < trx_list_get_min_trx_id()
&& !recv_recovery_is_on()) {
return(NULL);
}
03 – 鎖的實(shí)現(xiàn)
– 鎖的存放
A. table->locks 存放一個(gè)表的所有表級鎖。
B. lock_sys->rec_hash存放所有表的行鎖。Hash值根據(jù)(spaceid, pageno)來計(jì)算。
C. trx->trx_locks存放事務(wù)的所有鎖,包括表級鎖和行級鎖。一個(gè)事務(wù)的所有鎖,在事務(wù)結(jié)束時(shí),一起釋放。代碼在lock_release_off_kernel().如果有等待的鎖可以被授權(quán),則會(huì)將等待的鎖,轉(zhuǎn)變?yōu)楸皇跈?quán)的鎖,并喚醒相應(yīng)的事務(wù)。
– 行鎖的唯一識別
第一印象想到的是,用每行記錄的鍵值來做行鎖的唯一識別.但是鍵值占用空間比較大。
InnoDB使用Page NO.+Heap NO.來做行鎖的唯一識別。我們可以將Heap no.理解為頁面上的一個(gè)自增數(shù)值。每條物理記錄在被創(chuàng)建時(shí),都會(huì)分配一個(gè)唯一的heap no.
A. 鍵值可以理解為一個(gè)邏輯值,page no. + heap no. 是物理的。
B. 物理的雖然占用空間小,但是處理要復(fù)雜一些。如:在分裂一個(gè)B+Tree頁面時(shí),一半的記錄要移到新的頁面中,因此要對存在的鎖進(jìn)行遷移。
鎖移動(dòng)的d函數(shù)有:lock_move_reorganize_page(), lock_move_rec_list_start(),
lock_move_rec_list_end().
在刪除和插入數(shù)據(jù)時(shí),也要進(jìn)行GAP鎖的繼承。lock_rec_inherit_to_gap()
lock_rec_inherit_to_gap_if_gap_lock().
– 死鎖(Deadlock)
A. 超時(shí)機(jī)制。當(dāng)要加的鎖和其他鎖沖突時(shí),添加一個(gè)waiting鎖,并且返回DB_LOCK_WAIT錯(cuò)誤。
row_mysql_handle_error調(diào)用srv_suspend_mysql_thread來掛起一個(gè)線程。
B. 死鎖檢測檢測機(jī)制。每當(dāng)創(chuàng)建waiting鎖,都要調(diào)用lock_deadlock_occurs()進(jìn)行死鎖的檢測。
死鎖檢測方法是Waits-For Graph.在lock_deadlock_recursive()中實(shí)現(xiàn)。
當(dāng)發(fā)現(xiàn)死鎖后要選擇其中的一個(gè)事務(wù),將其回滾,來解除死鎖。選擇哪一個(gè)事務(wù)回滾能?
– 如果一個(gè)事務(wù)修改了non-transactional表(如MyISAM表,修改不能回滾),另一個(gè)表沒有。
則沒有修改non-transactional的會(huì)被回滾。
– 如果2個(gè)事務(wù)都修改了non-transactional表或者都沒有。則比較2個(gè)事務(wù)修改的記錄數(shù)和加的鎖數(shù)量。總和小的事務(wù)會(huì)被回滾。trx_weight_ge()實(shí)現(xiàn)這個(gè)邏輯。
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/4887143.html
總結(jié)
以上是生活随笔為你收集整理的MySQL数据库InnoDB存储引擎中的锁机制--转载的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql测试spring事务是否生效
- 下一篇: Streaming Big Data: