from:http://www.fanyilun.me/2017/04/20/MySQL%E5%8A%A0%E9%94%81%E5%88%86%E6%9E%90/

MySQL加鎖分析


前言

  最近遇到一次MySQL死鎖的問(wèn)題,也算是少見(jiàn)的一件事情。公司的MySQL隔離級(jí)別是Read Commited,已經(jīng)沒(méi)有了gap lock,而且代碼里的sql都再簡(jiǎn)單不過(guò),沒(méi)有顯式加鎖的sql語(yǔ)句。因此抽出時(shí)間看了一下原因。
  分析具體問(wèn)題之前,先整體的了解一下MySQL的加鎖邏輯,之后再分析起來(lái)就游刃有余了:

MySQL的鎖

  為什么MySQL要加鎖呢?OLTP數(shù)據(jù)庫(kù)離不開(kāi)事務(wù),事務(wù)也離不開(kāi)并發(fā)操作下一致性的問(wèn)題。現(xiàn)代數(shù)據(jù)庫(kù)解決事務(wù)的并發(fā)控制有兩種辦法,2PL和MVCC[1]。
  2PL是加鎖方案的代表,就是將數(shù)據(jù)操作分為加鎖和解鎖兩個(gè)階段,任何數(shù)據(jù)操作都會(huì)將訪問(wèn)對(duì)象加上鎖,后續(xù)對(duì)這個(gè)對(duì)象的數(shù)據(jù)操作就會(huì)被阻塞直到鎖釋放(事務(wù)提交)。傳統(tǒng)數(shù)據(jù)庫(kù)大都是用2PL來(lái)實(shí)現(xiàn)并發(fā)控制的。
  MVCC(多版本并發(fā)控制)是無(wú)鎖方案的代表,通過(guò)對(duì)數(shù)據(jù)庫(kù)每一次變更記錄版本快照,實(shí)現(xiàn)讀-寫(xiě)互不阻塞,寫(xiě)-寫(xiě)是否阻塞取決于具體實(shí)現(xiàn)(例如postgres的SERIALIZABLE級(jí)別下寫(xiě)-寫(xiě)互不阻塞,發(fā)生沖突拋出異常)。
  對(duì)于MySQL(innoDB)來(lái)說(shuō),是通過(guò)MVCC實(shí)現(xiàn)讀-寫(xiě)并發(fā)控制,又是通過(guò)2PL寫(xiě)-寫(xiě)并發(fā)控制的,因此依然保留著(悲觀)鎖這個(gè)概念,既然有悲觀鎖,自然就有可能產(chǎn)生死鎖問(wèn)題。
  MySQL的事務(wù)我之前在這篇文章里做過(guò)一些粗淺的理解:傳送門(mén) (痛心的是網(wǎng)上大部分資料還是顯示mySql在RR隔離級(jí)別下會(huì)幻讀。。)

那么MySQL會(huì)如何加鎖呢[2]:

MySQL鎖的模式:

  • 共享/排它鎖 (S鎖/X鎖) (Shared and Exclusive Locks)
    • S鎖與X鎖沖突,S鎖與S鎖不沖突,X鎖和X鎖沖突
      • 鎖沖突意味著無(wú)法獲取鎖的事務(wù)需要等待,鎖被釋放后才能繼續(xù)。當(dāng)然也有可能等待超時(shí)或檢測(cè)出死鎖
    • 快照讀(普通select …)不加鎖
    • select..lock in share mode / Serializable下的select 會(huì)加S鎖
    • select..for update / 寫(xiě)操作(insert update delete) 會(huì)加X(jué)鎖
    • 上述的鎖都是行級(jí)別的,S鎖和X鎖同樣可以加在表級(jí)別上,對(duì)應(yīng)的語(yǔ)句分別是LOCK TABLE … READ和LOCK TABLE … WRITE
  • 意向鎖 (IS鎖/IX鎖) (Intention Locks)
    • 意向鎖是表級(jí)別的鎖,用來(lái)標(biāo)識(shí)該表上面有數(shù)據(jù)被鎖住(或即將被鎖)
    • 一個(gè)事務(wù)在獲取(任何一行/或者全表)S鎖之前,一定會(huì)先在所在的表上加IS鎖。同理,獲取X鎖之前一定會(huì)加上IX鎖。
    • 意向鎖提出的目的,就是要標(biāo)識(shí)這個(gè)表上面有鎖,這樣一來(lái),對(duì)于表級(jí)別鎖的請(qǐng)求(LOCK TABLE …),就可以直接判斷是否有鎖沖突,而不需要逐行檢查鎖的狀態(tài)了。從更大的角度來(lái)看,意向鎖就是為了實(shí)現(xiàn)不同粒度的鎖共存,每次加鎖都需要先對(duì)上面更粗粒度的數(shù)據(jù)結(jié)構(gòu)加意向鎖,用來(lái)表達(dá)“這個(gè)數(shù)據(jù)結(jié)構(gòu)中存在被鎖住的數(shù)據(jù)”。

其兼容矩陣如下(+表示兼容,-表示沖突):

\ISIXSX
IS+++
IX++
S++
X

  上面提到的鎖的模式,指的是如何鎖住數(shù)據(jù),各種模式之間是否兼容;下面提到的鎖的類(lèi)型,定義的是具體鎖在哪里。二者并不沖突,比如record lock可以分成record x lock和record s lock。

MySQL鎖的類(lèi)型:

  • Record Locks
    • 對(duì)單條索引記錄上加的鎖。準(zhǔn)確的說(shuō),鎖是加在索引上的而非行上。因?yàn)閕nnodb一定會(huì)有一個(gè)聚簇索引,因此最終的行鎖都會(huì)落到聚簇索引上。
    • 可以加在聚簇索引或者二級(jí)索引上。
  • Gap Locks
    • gap lock是對(duì)索引間隙加的鎖,可以是在一條索引記錄之前,也可以在一條索引記錄之后。
    • gap lock的唯一作用,就是阻止其他事務(wù)向鎖住的gap里插入數(shù)據(jù)。
    • gap lock下的所有鎖的模式都是兼容的,比如同一個(gè)位置的gap s lock和gap x lock是可以共存的。其作用也是完全相同的。
    • 在READ COMMITTED隔離級(jí)別下,不會(huì)使用gap lock。因此下文關(guān)于gap lock的加鎖,對(duì)于RC隔離級(jí)別可以自動(dòng)忽略。
  • Next-Key Locks
    • Next-Key lock與record lock加鎖的粒度一樣,都是加在一條索引記錄上的。一個(gè)next-key lock=對(duì)應(yīng)的索引記錄的record lock+該索引前面的間隙的gap lock
    • 雖然說(shuō)Next-Key Lock代表著record lock+前一個(gè)間隙的gap lock,在必要的情況下,最后一條記錄后面的gap也有可能作為一條單獨(dú)的gap lock被鎖住[3]。
    • 由于鎖住的是前面的間隙,所以有些資料也會(huì)用左開(kāi)右閉的區(qū)間來(lái)表示next-key lock,例如(1,3]
  • Insert Intention Locks
    • Insert Intention Lock是一種特殊的間隙鎖,執(zhí)行insert之前會(huì)向插入的間隙加上Insert Intention Lock
    • Insert Intention Lock與已有的gap lock沖突,因此gap lock鎖住的間隙是不能插入數(shù)據(jù)的
    • Insert Intention Lock與Insert Intention Lock之間不沖突,因此允許了同時(shí)向同一個(gè)間隙插入不同主鍵的數(shù)據(jù)

其兼容矩陣如下,+表示兼容,-表示沖突:

要加的鎖\ 已存在的鎖record lockgap lockinsert intention locknext key lock
record lock++
gap lock++++
insert intention lock++
next key lock++

如何查看事務(wù)的加鎖情況

  當(dāng)存在鎖沖突/等待時(shí),比較方便的查看鎖沖突的方式:

1
2
3
4
// innodb_locks記錄了所有innodb正在等待的鎖,和被等待的鎖
select * from information_schema.innodb_locks;
// innodb_lock_waits記錄了所有innodb鎖的持有和等待關(guān)系
select * from information_schema.innodb_lock_waits'

image
  結(jié)果如上圖,可以看到當(dāng)前事務(wù)id 4579持有著’new_table’表的聚簇索引=3的X鎖。事務(wù)id 4580正在等待’new_table’表的聚簇索引=3的X鎖。

  但是上述方式只能看到存在鎖沖突的記錄,不能看到每個(gè)事務(wù)實(shí)際鎖住的記錄和范圍。因此更通用的辦法是,直接打開(kāi)innodb的鎖監(jiān)控,在控制臺(tái)查看詳細(xì)鎖狀態(tài):

1
2
mysql> set global innodb_status_output=ON; // 可選。將監(jiān)控輸出到log_error輸出中,15秒刷新一次
mysql> set global innodb_status_output_locks=ON; // 輸出的內(nèi)容包含鎖的詳細(xì)信息

  通過(guò)show engine innodb status;語(yǔ)句,可以輸出每個(gè)事務(wù)當(dāng)前持有的鎖結(jié)果,常見(jiàn)的結(jié)果類(lèi)型解釋如下。死鎖日志也會(huì)記錄如下的鎖記錄,因此可以用同樣的方式來(lái)讀MySQL的死鎖日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 表示事務(wù)4641對(duì)表`sys`.`new_table`持有了IX鎖
TABLE LOCK table `sys`.`new_table` trx id 4641 lock mode IX
// space id=38,space id可以唯一確定一張表,表示了鎖所在的表
// page no 3,表示鎖所在的頁(yè)號(hào)
// index PRIMARY 表示鎖位于名為PRIMARY的索引上
// lock_mode X locks rec but not gap 表示x record lock
// 下方的數(shù)據(jù)表示了被鎖定的索引數(shù)據(jù),最上面一行代表索引列的十六進(jìn)制值,在這里表示的就是id=3的數(shù)據(jù)
RECORD LOCKS space id 38 page no 3 n bits 80 index PRIMARY of table `sys`.`new_table` trx id 4641 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 8; compact format; info bits 0
0: len 4; hex 00000003; asc ;;
1: len 6; hex 0000000011e9; asc ;;
2: len 7; hex a70000011b0128; asc (;;
3: len 4; hex 8000012c; asc ,;;
4: len 1; hex 63; asc c;;
5: len 4; hex 80000006; asc ;;
6: len 3; hex 636363; asc ccc;;
7: len 2; hex 3333; asc 33;;
// lock_mode X表示的是next-key lock,即當(dāng)前記錄的record lock+前一個(gè)間隙的gap lock
// 這個(gè)鎖在名為idx1的索引上,對(duì)應(yīng)的索引列的值為100(hex 64對(duì)應(yīng)十進(jìn)制),對(duì)應(yīng)聚簇索引的值為1
RECORD LOCKS space id 38 page no 5 n bits 80 index idx1 of table `sys`.`new_table` trx id 4643 lock_mode X
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 00000064; asc d;;
1: len 4; hex 00000001; asc ;;
// lock_mode X locks gap before rec表示的是對(duì)應(yīng)索引記錄前一個(gè)間隙的gap lock
RECORD LOCKS space id 38 page no 5 n bits 80 index idx1 of table `sys`.`new_table` trx id 4643 lock_mode X locks gap before rec
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 800000c8; asc ;;
1: len 4; hex 00000002; asc ;;

不同語(yǔ)句的加鎖情況

以下實(shí)驗(yàn)數(shù)據(jù)基于MySQL 5.7。
假設(shè)已知一張表my_table,id列為主鍵。

idnamenum
1aaa100
5bbb200
8bbb300
10ccc400

對(duì)該表進(jìn)行讀寫(xiě)操作,可能產(chǎn)生的加鎖情況如下(僅考慮隔離級(jí)別為RR和RC):

1. 查詢(xún)命中聚簇索引(主鍵索引)

1.1 如果是精確查詢(xún),那么會(huì)在命中的索引上加record lock。
例如:

1
2
3
4
5
// 在id=1的聚簇索引上加X(jué)鎖
update my_table set name='a' where id=1;
// 在id=1的聚簇索引上加S鎖
select * from my_table where id=1 lock in share mode;

1.2 如果是范圍查詢(xún),那么

  • 1.2.1 在RC隔離級(jí)別下,會(huì)在所有命中的行的聚簇索引上加record locks(只鎖行)
1
2
3
4
5
// 在id=8和10的聚簇索引上加X(jué)鎖
update my_table set name='a' where id>7;
// 在id=1的聚簇索引上加X(jué)鎖
update my_table set name='a' where id<=1;
  • 1.2.2 在RR隔離級(jí)別下,會(huì)在所有命中的行的聚簇索引上加next-key locks(鎖住行和間隙)。最后命中的索引的后一條記錄,也會(huì)被加上next-key lock。
1
2
3
4
5
6
7
// 在id=8、10(、+∞)的聚簇索引上加X(jué)鎖
// 在(5,8)(8,10)(10,+∞)加gap lock
update my_table set name='a' where id>7;
// 在id=1、5的聚簇索引上加X(jué)鎖
// 在(-∞,1)(1,5)加gap lock
update my_table set name='a' where id<=1;

1.3 如果查詢(xún)結(jié)果為空,那么

  • 1.2.1 在RC隔離級(jí)別下,什么也不會(huì)鎖

  • 1.2.2 在RR隔離級(jí)別下,會(huì)鎖住查詢(xún)目標(biāo)所在的間隙。

1
2
// 在(1,5)加gap lock
update my_table set name='a' where id=2;

2. 查詢(xún)命中唯一索引

假設(shè)上述表中,num列加了唯一索引
2.1 如果是精確查詢(xún),那么會(huì)在命中的唯一索引,和對(duì)應(yīng)的聚簇索引上加record lock。

1
2
3
// 在num=100的唯一索引上加X(jué)鎖
// 并在id=1的聚簇索引上加X(jué)鎖
update my_table set name='a' where num=100;

2.2 如果是范圍查詢(xún),那么

  • 2.2.1 在RC隔離級(jí)別下,會(huì)在所有命中的唯一索引和聚簇索引上加record lock。同2.1
  • 2.2.2 在RR隔離級(jí)別下,會(huì)在所有命中的行的唯一索引上加next-key locks。最后命中的索引的后一條記錄,也會(huì)被加上next-key lock。
1
2
3
4
// 在num=100和num=200的唯一索引上加X(jué)鎖
// 并在id=1和id=5的聚簇索引上加X(jué)鎖
// 并在唯一索引的間隙(-∞,100)(100,200)加gap lock
update my_table set name='a' where num<150;

2.3 如果查詢(xún)結(jié)果為空,同1.3。唯一差別在于,此時(shí)加的gap lock是位于唯一索引上的。

3. 查詢(xún)命中二級(jí)索引(非唯一索引)

假設(shè)上述表中,name列加了普通二級(jí)索引,num列沒(méi)有索引
3.1 如果是精確查詢(xún),那么

  • 3.1.1 在RC隔離級(jí)別下,同2.1,對(duì)命中的二級(jí)索引和聚簇索引加record lock
1
2
3
// 在name='bbb'的兩條索引記錄上加X(jué)鎖
// 并在id=5和id=8的聚簇索引上加X(jué)鎖
update my_table set num=10 where name='bbb';
  • 3.1.2 在RR隔離級(jí)別下,會(huì)在命中的二級(jí)索引上加next-key lock,最后命中的索引的后面的間隙會(huì)加上gap lock。對(duì)應(yīng)的聚簇索引上加record lock。
1
2
3
4
// 在name='bbb'的兩條索引記錄上加X(jué)鎖
// 并在id=5和id=8的聚簇索引上加X(jué)鎖
// 并在二級(jí)索引的間隙('aaa','bbb')('bbb','bbb')('bbb','ccc')加gap lock
update my_table set num=10 where name='bbb';

3.2 范圍查詢(xún)、模糊查詢(xún)的情況比較復(fù)雜,此處不詳述。可以用上述方法自己實(shí)驗(yàn)。

4. 查詢(xún)沒(méi)有命中索引

假設(shè)上述表中,name列加了普通二級(jí)索引,num列沒(méi)有索引
4.1 如果查詢(xún)條件沒(méi)有命中索引

  • 4.1.1 在RC隔離級(jí)別下,對(duì)命中的數(shù)據(jù)的聚簇索引加X(jué)鎖。根據(jù)MySQL官方手冊(cè)[4],對(duì)于update和delete操作,RC只會(huì)鎖住真正執(zhí)行了寫(xiě)操作的記錄,這是因?yàn)楸M管innodb會(huì)鎖住所有記錄,MySQL Server層會(huì)進(jìn)行過(guò)濾并把不符合條件的鎖當(dāng)即釋放掉[5]。同時(shí)對(duì)于UPDATE語(yǔ)句,如果出現(xiàn)了鎖沖突(要加鎖的記錄上已經(jīng)有鎖),innodb不會(huì)立即鎖等待,而是執(zhí)行semi-consistent read:返回改數(shù)據(jù)上一次提交的快照版本,供MySQL Server層判斷是否命中,如果命中了才會(huì)交給innodb鎖等待。因此加鎖情況可以這樣來(lái)認(rèn)為:
1
2
3
4
5
6
// 在id=5的聚簇索引上加X(jué)鎖
update my_table set num=1 where num=200;
// 先在id=1,5,8,10(全表所有記錄)的聚簇索引上加X(jué)鎖
// 然后馬上釋放id=1,8,10的鎖,只保留id=5的鎖
delete from my_table where num=200;
  • 4.1.2 在RR隔離級(jí)別下,事情就很糟糕了,對(duì)全表的所有聚簇索引數(shù)據(jù)加next-key lock
1
2
3
4
5
6
// 在id=1,5,8,10(全表所有記錄)的聚簇索引上加X(jué)鎖
// 并在聚簇索引的所有間隙(-∞,1)(1,5)(5,8)(8,10)(10,+∞)加gap lock
update my_table set num=100 where num=200;
// 盡管name列有索引,但是like '%%'查詢(xún)不使用索引,因此此時(shí)也是鎖住所有聚簇索引,情況和上面一模一樣
update my_table set num=100 where name like '%b%';

5. 對(duì)索引鍵值有修改

假設(shè)上述表中,num列加了二級(jí)索引
  如果一條update語(yǔ)句,對(duì)索引鍵值有修改,那么修改前后的數(shù)據(jù)如何加鎖呢。這點(diǎn)要結(jié)合數(shù)據(jù)多版本的可見(jiàn)性來(lái)考慮:無(wú)論是聚簇索引,還是二級(jí)索引,只要其鍵值更新,就會(huì)產(chǎn)生新版本。將老版本數(shù)據(jù)deleted bti設(shè)置為1;同時(shí)插入新版本[6]。因此可以認(rèn)為,一次索引鍵值的修改實(shí)際上操作了兩條索引數(shù)據(jù):原索引和修改后的新索引。
  從innodb的事務(wù)的角度來(lái)看,如果一個(gè)事務(wù)操作(寫(xiě))了一條數(shù)據(jù),那么這條數(shù)據(jù)一定要加鎖。因此可以認(rèn)為,如果修改了索引鍵值,那么修改前和修改后的索引都會(huì)加鎖。另外,由于修改的數(shù)據(jù)并沒(méi)有被作為查詢(xún)條件,那么也不會(huì)有“不可重復(fù)讀”和“幻讀”的問(wèn)題,因此無(wú)需加gap lock,索引修改只會(huì)加X(jué) record lock。

示例(RC和RR級(jí)別效果一樣):

1
2
3
// 在id=1的聚簇索引上加X(jué)鎖
// 并在name='aaa'(name列索引原鍵值)和name='eee'(新鍵值)的索引上加鎖
update my_table set name='eee' where id=1;

6. 插入數(shù)據(jù)

假設(shè)上述表中,num列加了二級(jí)索引
insert加鎖過(guò)程:

  1. 唯一索引沖突檢查:表中一定有至少一個(gè)唯一索引,那么首先會(huì)做唯一索引的沖突檢查。innodb檢查唯一索引沖突的方式是,對(duì)目標(biāo)的索引項(xiàng)加S鎖(因?yàn)椴荒芤蕾?lài)快照讀,需要一個(gè)徹底的當(dāng)前讀),讀到數(shù)據(jù)則唯一索引沖突,返回異常,否則檢查通過(guò)。
  2. 對(duì)插入的間隙加上插入意向鎖(Insert Intention Lock)
  3. 對(duì)插入記錄的所有索引項(xiàng)加X(jué)鎖

示例:

1
2
3
4
5
// 先對(duì)id=15加S鎖
// 再對(duì)間隙id(10,+∞)和name('ccc',+∞)加Insert Intention Lock
// 然后在id=15的聚簇索引上加X(jué)鎖(S鎖升級(jí)為X鎖)
// 并在name='fff'的索引上加X(jué)鎖
insert into my_table (`id`, `name`, `num`) values ('15', 'fff', '800');

  還有一個(gè)有趣的問(wèn)題,如果插入的二級(jí)索引鍵值已經(jīng)存在,那么這個(gè)插入意向鎖會(huì)加在哪個(gè)間隙中呢?
  顧名思義,插入意向鎖鎖定的間隙一定是將要插入的索引的位置,如果二級(jí)索引鍵值相同,默認(rèn)會(huì)按照聚簇索引的大小來(lái)排序(二級(jí)索引在存儲(chǔ)上其實(shí)就是{索引值,主鍵值})。例如:

1
2
3
4
5
6
7
8
// 插入意向鎖加在間隙 ({'aaa',1},{'bbb',5}) 上
insert into my_table (`id`, `name`, `num`) values ('4', 'bbb', '800');
// 插入意向鎖加在間隙 ({'bbb',5},{'bbb',8}) 上
insert into my_table (`id`, `name`, `num`) values ('6', 'bbb', '800');
// 插入意向鎖加在間隙 ({'bbb',8},{'ccc',10}) 上
insert into my_table (`id`, `name`, `num`) values ('11', 'bbb', '800');

隱式鎖

  為了降低鎖的開(kāi)銷(xiāo),innodb采用了延遲加鎖機(jī)制,即隱式鎖(implicit lock)[7]。
  從數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)上看,每張表的數(shù)據(jù)都是掛在聚簇索引的B+樹(shù)下面的葉子節(jié)點(diǎn)上(每個(gè)節(jié)點(diǎn)代表一個(gè)page,每個(gè)page存放著多行數(shù)據(jù))。每行存儲(chǔ)的信息項(xiàng)中都會(huì)存有一隱藏列事務(wù)id。當(dāng)有事務(wù)對(duì)這條記錄進(jìn)行修改時(shí),需要先判斷該行記錄是否有隱式鎖(原記錄的事務(wù)id是否是活動(dòng)的事務(wù)),如果有則為其真正創(chuàng)建鎖并等待,否則直接更新數(shù)據(jù)并寫(xiě)入自己的事務(wù)id。
  二級(jí)索引雖然存儲(chǔ)上沒(méi)有記錄事務(wù)id,但同樣可以存在隱式鎖,只不過(guò)判斷邏輯復(fù)雜一些,需要依賴(lài)對(duì)應(yīng)的聚簇索引做計(jì)算。
  當(dāng)然,隱式鎖只是一個(gè)實(shí)現(xiàn)細(xì)節(jié),顯示還是隱式加鎖并不影響上文對(duì)加鎖的判斷。
  另外,聚簇索引每行記錄的事務(wù)id,還有一個(gè)重要作用就是實(shí)現(xiàn)MVCC快照讀:由于事務(wù)id是全局遞增的,那么進(jìn)行快照讀的時(shí)候,如果數(shù)據(jù)的事務(wù)id小于當(dāng)前事務(wù)id并且不在活躍事務(wù)列表內(nèi)(尚未提交),則直接返回當(dāng)前行數(shù)據(jù)。否則需要根據(jù)roll pointer(和事務(wù)id一樣,也在每行的隱藏列中)去查找undo日志。

一個(gè)RC隔離級(jí)別下的死鎖

  其實(shí)可以看到,RC隔離級(jí)別下的加鎖已經(jīng)很少了,用官方文檔的話(huà)說(shuō)”greatly reduces the probability of deadlocks”。因此盡管MySQL的默認(rèn)隔離級(jí)別是RR,但是互聯(lián)網(wǎng)應(yīng)用更傾向與使用RC來(lái)避免死鎖+提高并發(fā)能力。例如阿里電商的MySQL默認(rèn)級(jí)別就是RC。
  尷尬的是,但是我也的的確確碰到了RC的死鎖。還是以這個(gè)表來(lái)舉例,假設(shè)id為主鍵,num列無(wú)索引。

idnamenum
1aaa100
5bbb200
8bbb300

按以下順序執(zhí)行事務(wù):

trx1trx2
insert into my_table (id, name, num) values (‘16’, ‘rrr’, ‘888’);-
-insert into my_table (id, name, num) values (‘17’, ‘ttt’, ‘999’);
delete from sys.my_table where num=300; // waiting-
-delete from sys.my_table where num=400; // deadlock

  對(duì)照上文的加鎖邏輯,insert會(huì)對(duì)聚簇索引加X(jué)鎖,因此trx1和trx2首先會(huì)分別持有id=16和id=17的X鎖。
  接下來(lái)坑爹的事情來(lái)了,對(duì)于無(wú)索引字段,delete操作不會(huì)執(zhí)行semi-consistent read,而是先直接鎖住所有數(shù)據(jù)的聚簇索引(盡管后面會(huì)馬上釋放,但也需要先獲取鎖)。這樣一來(lái),事務(wù)1的delete需要鎖住所有記錄,等待事務(wù)2持有的id=17的X鎖,而事務(wù)2的delete需要等待事務(wù)1的id=16的X鎖。死鎖就產(chǎn)生了。
  在這個(gè)例子中,如果insert和delete的順序都顛倒一下,或者delete都變?yōu)閡pdate,死鎖都不會(huì)發(fā)生。

小結(jié)

  • 索引記錄的間隙上用來(lái)避免幻讀。
  • Select(Serializable隔離級(jí)別除外)不會(huì)加鎖,而是執(zhí)行快照讀。
  • 寫(xiě)操作都會(huì)加鎖,具體加鎖方式取決于隔離級(jí)別、索引命中情況以及修改的索引情況。
  • 為了減少鎖的范圍,避免死鎖的發(fā)生,應(yīng)該盡量讓查詢(xún)條件命中索引,而且命中的越精確加鎖越少。同時(shí)如果能接受RC級(jí)別對(duì)一致性的破壞,可以將隔離級(jí)別調(diào)整成RC。

參考資料

[1] 蕭美陽(yáng), 葉曉俊. 并發(fā)控制實(shí)現(xiàn)方法的比較研究[J]. 計(jì)算機(jī)應(yīng)用研究, 2006, 23(6):19-22.
[2] MySQL 5.7 Reference Manual :: 15.5.1 InnoDB Locking
[3] MySQL 5.7 Reference Manual :: 15.5.4 Phantom Rows
[4] MySQL 5.7 Reference Manual :: 15.5.2.1 Transaction Isolation Levels
[5] MySQL 加鎖處理分析
[6] InnoDB多版本(MVCC)實(shí)現(xiàn)簡(jiǎn)要分析
[7] Introduction to Transaction Locks in InnoDB Storage Engine