InnoDB的鎖機(jī)制淺析
1. 前言
數(shù)據(jù)事務(wù)設(shè)計(jì)遵循ACID的原則用僧。
MySQL數(shù)據(jù)庫(kù)提供了四種默認(rèn)的隔離級(jí)別,讀未提交(read-uncommitted)瑟由、讀已提交(或不可重復(fù)讀)(read-committed)潮瓶、可重復(fù)讀(repeatable-read)、串行化(serializable)儒溉。
MySQL的默認(rèn)隔離級(jí)別是RR。
2. 鎖基本概念
2.1 共享鎖和排它鎖
InnoDB實(shí)現(xiàn)了兩種標(biāo)準(zhǔn)行級(jí)鎖发钝,一種是共享鎖(shared locks顿涣,S鎖)波闹,另一種是獨(dú)占鎖,或者叫排它鎖(exclusive locks涛碑,X鎖)精堕。
S鎖允許當(dāng)前持有該鎖的事務(wù)讀取行。
X鎖允許當(dāng)前持有該鎖的事務(wù)更新或刪除行蒲障。
S鎖
如果事務(wù)T1持有了行r上的S鎖
歹篓,則其他事務(wù)可以同時(shí)持有行r的S鎖
,但是不能對(duì)行r加X鎖
晌涕。
X鎖
如果事務(wù)T1持有了行r上的X鎖
滋捶,則其他任何事務(wù)不能持有行r的X鎖
痛悯,必須等待T1在行r上的X鎖
釋放余黎。
如果事務(wù)T1在行r上保持S鎖
,則另一個(gè)事務(wù)T2對(duì)行r的鎖的請(qǐng)求按如下方式處理:
- T2可以同時(shí)持有S鎖
- T2如果想在行r上獲取
X鎖
载萌,必須等待其他事務(wù)對(duì)該行添加的S鎖
或X鎖
的釋放惧财。
2.2 意向鎖-Intention Locks
InnoDB支持多種粒度的鎖,允許行級(jí)鎖和表級(jí)鎖的共存扭仁。例如LOCK TABLES ... WRITE
等語(yǔ)句可以在指定的表上加上獨(dú)占鎖垮衷。
InnoBD使用意向鎖來實(shí)現(xiàn)多個(gè)粒度級(jí)別的鎖定。意向鎖是表級(jí)鎖乖坠,表示table中的row所需要的鎖(S鎖或X鎖)的類型搀突。
意向鎖分為意向共享鎖(IS鎖)和意向排它鎖(IX鎖)。
IS鎖表示當(dāng)前事務(wù)意圖在表中的行上設(shè)置共享鎖熊泵,下面語(yǔ)句執(zhí)行時(shí)會(huì)首先獲取IS鎖仰迁,因?yàn)檫@個(gè)操作在獲取S鎖:
SELECT ... LOCK IN SHARE MODE
IX鎖表示當(dāng)前事務(wù)意圖在表中的行上設(shè)置排它鎖。下面語(yǔ)句執(zhí)行時(shí)會(huì)首先獲取IX鎖顽分,因?yàn)檫@個(gè)操作在獲取X鎖:
SELECT ... FOR UPDATE
事務(wù)要獲取某個(gè)表上的S鎖和X鎖之前徐许,必須先分別獲取對(duì)應(yīng)的IS鎖和IX鎖。
2.3 鎖的兼容性
鎖的兼容矩陣如下:
--- | 排它鎖(X) | 意向排它鎖(IX) | 共享鎖(S) | 意向共享鎖(IS) |
---|---|---|---|---|
排它鎖(X) | N | N | N | N |
意向排它鎖(IX) | N | OK | N | OK |
共享鎖(S) | N | N | OK | OK |
意向共享鎖(IS) | N | OK | OK | OK |
按照上面的兼容性卒蘸,如果不同事務(wù)之間的鎖兼容雌隅,則當(dāng)前加鎖事務(wù)可以持有鎖,如果有沖突則會(huì)等待其他事務(wù)的鎖釋放缸沃。
如果一個(gè)事務(wù)請(qǐng)求鎖時(shí)恰起,請(qǐng)求的鎖與已經(jīng)持有的鎖沖突而無法獲取時(shí),互相等待就可能會(huì)產(chǎn)生死鎖趾牧。
意向鎖不會(huì)阻止除了全表鎖定請(qǐng)求之外的任何鎖請(qǐng)求检盼。
意向鎖的主要目的是顯示事務(wù)正在鎖定某行或者正意圖鎖定某行。
3. InnoDB中的鎖
常見的鎖有Record鎖武氓、gap鎖梯皿、next-key鎖仇箱、插入意向鎖、自增鎖等东羹。
下面會(huì)對(duì)每一種鎖給出一個(gè)查看鎖的示例剂桥。
3.1 準(zhǔn)備工作
3.1.1 測(cè)試用表結(jié)構(gòu)
示例的基礎(chǔ)是一個(gè)只有兩列的數(shù)據(jù)庫(kù)表。
mysql> CREATE TABLE test (
id int(11) NOT NULL,
code int(11) NOT NULL,
PRIMARY KEY(id),
KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
mysql> INSERT INTO test(id,code) values(1,1),(10,10);
數(shù)據(jù)表test
只有兩列属提,id
是主鍵索引权逗,code
是普通的索引(注意,一定不要是唯一索引)冤议,并初始化了兩條記錄斟薇,分別是(1,1),(10,10)。
這樣恕酸,我們驗(yàn)證唯一鍵索引就可以使用id列堪滨,驗(yàn)證普通索引(非唯一鍵二級(jí)索引)時(shí)就使用code列。
3.1.2 查看鎖狀態(tài)的方式
要看到鎖的情況蕊温,必須手動(dòng)開啟多個(gè)事務(wù)袱箱,其中一些鎖的狀態(tài)的查看則必須使鎖處于waiting
狀態(tài),這樣才能在mysql的引擎狀態(tài)日志中看到义矛。
命令:
mysql> show engine innodb status;
這條命令能顯示最近幾個(gè)事務(wù)的狀態(tài)发笔、查詢和寫入情況等信息。當(dāng)出現(xiàn)死鎖時(shí)凉翻,命令能給出最近的死鎖明細(xì)了讨。
3.2 記錄鎖 Record Locks
Record鎖
Record Lock
是對(duì)索引記錄的鎖定。記錄鎖有兩種模式制轰,S模式和X模式前计。
例如SELECT id FROM test WHERE id = 10 FOR UPDATE;
表示防止任何其他事務(wù)插入、更新或者刪除id =10
的行艇挨。
記錄鎖始終只鎖定索引残炮。即使表沒有建立索引,InnoDB也會(huì)創(chuàng)建一個(gè)隱藏的聚簇索引(隱藏的遞增主鍵索引)缩滨,并使用此索引進(jìn)行記錄鎖定势就。
查看記錄鎖
開啟第一個(gè)事務(wù),不提交脉漏,測(cè)試完之后回滾苞冯。
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set id=2 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
事務(wù)加鎖情況
mysql> show engine innodb status\G;
...
------------
TRANSACTIONS
------------
---TRANSACTION 366811, ACTIVE 690 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 2
MySQL thread id 785, OS thread handle 123145432457216, query id 729076 localhost 127.0.0.1 root
...
可以看到有一行被加了鎖。由之前對(duì)鎖的描述可以推測(cè)出侧巨,update語(yǔ)句給id=1
這一行上加了一個(gè)X鎖
舅锄。
注意:X鎖廣義上是一種抽象意義的排它鎖,即鎖一般分為
X模式
和S模式
司忱,狹義上指row或者index上的鎖皇忿,而Record鎖是索引上的鎖畴蹭。
為了不修改數(shù)據(jù),可以用select ... for update
語(yǔ)句鳍烁,加鎖行為和update
叨襟、delete
是一樣的,insert
加鎖機(jī)制較為復(fù)雜幔荒,后面的章節(jié)會(huì)提到糊闽。
第一個(gè)事務(wù)保持原狀,不要提交或者回滾爹梁,現(xiàn)在開啟第二個(gè)事務(wù)右犹。
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set id=3 where id=1;
執(zhí)行update
時(shí),sql語(yǔ)句的執(zhí)行被阻塞了姚垃。查看下事務(wù)狀態(tài):
mysql> show engine innodb status\G;
...
------- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 62 page no 3 n bits 72 index PRIMARY of table `test`.`test` trx id 366820 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
0: len 8; hex 0000000000000001; asc ;;
1: len 6; hex 0000000598e3; asc ;;
2: len 7; hex 7e000001a80896; asc ~ ;;
------------------
...
喜聞樂見念链,我們看到了這個(gè)鎖的狀態(tài)。狀態(tài)標(biāo)題是'事務(wù)正在等待獲取鎖'莉炉,描述中的lock_mode X locks rec but not gap
就是本章節(jié)中的record記錄鎖钓账,直譯一下'X鎖模式鎖住了記錄'碴犬。后面還有一句but not gap
意思是只對(duì)record本身加鎖絮宁,并不對(duì)間隙加鎖,間隙鎖的敘述見下一個(gè)章節(jié)服协。
3.3 間隙鎖 Gap Locks
間隙鎖
間隙鎖作用在索引記錄之間的間隔绍昂,又或者作用在第一個(gè)索引之前,最后一個(gè)索引之后的間隙偿荷。不包括索引本身窘游。
例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
這條語(yǔ)句阻止其他事務(wù)插入10和20之間的數(shù)字跳纳,無論這個(gè)數(shù)字是否存在忍饰。
間隙可以跨越0個(gè),單個(gè)或多個(gè)索引值寺庄。
間隙鎖是性能和并發(fā)權(quán)衡的產(chǎn)物艾蓝,只存在于部分事務(wù)隔離級(jí)別。
select * from table where id=1;
唯一索引可以鎖定一行斗塘,所以不需要間隙鎖鎖定赢织。
如果列沒有索引或者具有非唯一索引,該語(yǔ)句會(huì)鎖定當(dāng)前索引前的間隙馍盟。
在同一個(gè)間隙上于置,不同的事務(wù)可以持有上述兼容/沖突表中沖突的兩個(gè)鎖。例如贞岭,事務(wù)T1現(xiàn)在持有一個(gè)間隙S鎖八毯,T2可以同時(shí)在同一個(gè)間隙上持有間隙X鎖搓侄。
允許沖突的鎖在間隙上鎖定的原因是,如果從索引中清除一條記錄话速,則由不同事務(wù)在這條索引記錄上的加間隙鎖的動(dòng)作必須被合并休讳。
InnoDB中的間隙鎖的唯一目的是防止其他事務(wù)插入間隙。
間隙鎖是可以共存的尿孔,一個(gè)事務(wù)占用的間隙鎖不會(huì)阻止另一個(gè)事務(wù)獲取同一個(gè)間隙上的間隙鎖俊柔。
如果事務(wù)隔離級(jí)別改為RC,則間隙鎖會(huì)被禁用活合。
查看間隙鎖
按照官方文檔雏婶,where
子句查詢條件是唯一鍵且指定了值時(shí),只有record鎖白指,沒有g(shù)ap鎖留晚。
如果where
語(yǔ)句指定了范圍,gap鎖是存在的告嘲。
這里只測(cè)試驗(yàn)證一下當(dāng)指定非唯一鍵索引的時(shí)候错维,gap鎖的位置,按照文檔的說法橄唬,會(huì)鎖定當(dāng)前索引及索引之前的間隙赋焕。(指定了非唯一鍵索引,例如code=10,間隙鎖仍然存在)
開啟第一個(gè)事務(wù)仰楚,鎖定一條非唯一的普通索引記錄
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code = 10 for update;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.00 sec)
由于預(yù)存了兩條數(shù)據(jù)隆判,row(1,1)和row(10,10),此時(shí)這個(gè)間隙應(yīng)該是1<gap<10
僧界。我們先插入row(2,2)來驗(yàn)證下gap鎖的存在侨嘀,再插入row(0,0)來驗(yàn)證gap的邊界。
按照間隙鎖的官方文檔定義捂襟,
select * from test where code = 10 for update;
會(huì)鎖定code=10
這個(gè)索引咬腕,并且會(huì)鎖定code<10
的間隙。
開啟第二個(gè)事務(wù)葬荷,在code=10
之前的間隙中插入一條數(shù)據(jù)涨共,看下這條數(shù)據(jù)是否能夠插入。
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(2,2);
插入的時(shí)候闯狱,執(zhí)行被阻塞煞赢,查看引擎狀態(tài):
mysql> show engine innodb status\G;
...
---TRANSACTION 366864, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 793, OS thread handle 123145434963968, query id 730065 localhost 127.0.0.1 root update
insert into test values(2,2)
------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 63 page no 4 n bits 72 index code of table `test`.`test` trx id 366864 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 8; hex 800000000000000a; asc ;;
1: len 8; hex 000000000000000a; asc ;;
------------------
...
插入語(yǔ)句被阻塞了,lock_mode X locks gap before rec
哄孤,由于第一個(gè)事務(wù)鎖住了1到10之間的gap照筑,需要等待獲取鎖之后才能插入。
如果再開啟一個(gè)事務(wù),插入(0,0)
mysql> start transaction;
mysql> insert into test values(0,0);
Query OK, 1 row affected (0.00 sec)
可以看到:指定的非唯一建索引的gap鎖的邊界是當(dāng)前索引到上一個(gè)索引之間的gap凝危。
最后給出鎖定區(qū)間的示例,首先插入一條記錄(5,5)
mysql> insert into test values(5,5);
Query OK, 1 row affected (0.00 sec)
開啟第一個(gè)事務(wù):
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code between 1 and 10 for update;
+----+------+
| id | code |
+----+------+
| 1 | 1 |
| 5 | 5 |
| 10 | 10 |
+----+------+
3 rows in set (0.00 sec)
第二個(gè)事務(wù)波俄,試圖去更新code=5的行:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set code=4 where code=5;
執(zhí)行到這里,如果第一個(gè)事務(wù)不提交或者回滾的話蛾默,第二個(gè)事務(wù)一直等待直至mysql中設(shè)定的超時(shí)時(shí)間懦铺。
3.4 Next-key Locks
Next-key鎖
Next-key鎖實(shí)際上是Record鎖和gap鎖的組合。Next-key鎖是在下一個(gè)索引記錄本身和索引之前的gap加上S鎖或是X鎖(如果是讀就加上S鎖支鸡,如果是寫就加X鎖)冬念。
默認(rèn)情況下,InnoDB的事務(wù)隔離級(jí)別為RR牧挣,系統(tǒng)參數(shù)innodb_locks_unsafe_for_binlog
的值為false
急前。InnoDB使用next-key鎖對(duì)索引進(jìn)行掃描和搜索,這樣就讀取不到幻象行瀑构,避免了幻讀
的發(fā)生裆针。
幻讀是指在同一事務(wù)下,連續(xù)執(zhí)行兩次同樣的SQL語(yǔ)句寺晌,第二次的SQL語(yǔ)句可能會(huì)返回之前不存在的行世吨。
當(dāng)查詢的索引是唯一索引時(shí),Next-key lock會(huì)進(jìn)行優(yōu)化呻征,降級(jí)為Record Lock耘婚,此時(shí)Next-key lock僅僅作用在索引本身,而不會(huì)作用于gap和下一個(gè)索引上怕犁。
查看Next-key鎖
Next-key鎖的作用范圍
如上述例子边篮,數(shù)據(jù)表test
初始化了row(1,1),row(10,10),然后插入了row(5,5)奏甫。數(shù)據(jù)表如下:
mysql> select * from test;
+----+------+
| id | code |
+----+------+
| 1 | 1 |
| 5 | 5 |
| 10 | 10 |
+----+------+
3 rows in set (0.00 sec)
由于id
是主鍵、唯一索引凌受,mysql會(huì)做優(yōu)化阵子,因此使用code
這個(gè)非唯一鍵的二級(jí)索引來舉例說明。
對(duì)于code
胜蛉,可能的next-key鎖的范圍是:
(-∞,1]
(1,5]
(5,10]
(10,+∞)
開啟第一個(gè)事務(wù)挠进,在code=5
的索引上請(qǐng)求更新:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code=5 for update;
+----+------+
| id | code |
+----+------+
| 5 | 5 |
+----+------+
1 row in set (8.81 sec)
之前在gap鎖的章節(jié)中介紹了,code=5 for update
會(huì)在code=5
的索引上加一個(gè)record鎖誊册,還會(huì)在1<gap<5的間隙上加gap鎖×焱唬現(xiàn)在不再驗(yàn)證,直接插入一條(8,8):
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(8);
insert
處于等待執(zhí)行的狀態(tài)案怯,這就是next-key鎖
生效而導(dǎo)致的結(jié)果君旦。第一個(gè)事務(wù),鎖定了區(qū)間(1,5],由于RR的隔離級(jí)別下next-key鎖
處于開啟生效狀態(tài)金砍,又鎖定了(5,10]區(qū)間局蚀。所以插入SQL語(yǔ)句的執(zhí)行被阻塞。
解釋:在這種情況下恕稠,被鎖定的區(qū)域是
code=5
前一個(gè)索引到它的間隙琅绅,以及next-key的區(qū)域。code=5 for update
對(duì)索引的鎖定用區(qū)間表示鹅巍,gap鎖鎖定了(1,5)千扶,record鎖鎖定了{(lán)5}索引記錄,next-key鎖鎖住了(5,10]骆捧,也就是說整個(gè)(1,10]的區(qū)間被鎖定了县貌。由于是for update
,所以這里的鎖都是X鎖凑懂,因此阻止了其他事務(wù)中帶有沖突鎖定的操作執(zhí)行煤痕。
如果我們?cè)诘谝粋€(gè)事務(wù)中,執(zhí)行了code>8 for update
接谨,在掃描過程中摆碉,找到了code=10
,此時(shí)就會(huì)鎖住10之前的間隙(5到10之間的gap)脓豪,10本身(record)巷帝,和10之后的間隙(next-key)。此時(shí)另一個(gè)事務(wù)插入(6,6),(9,9)和(11,11)都是不被允許的扫夜,只有在前一個(gè)索引5及5之前的索引和間隙才能執(zhí)行插入(更新和刪除也會(huì)被阻塞)楞泼。
3.5 插入意向鎖 Insert Intention Locks
插入意向鎖在行插入之前由INSERT設(shè)置一種間隙鎖,是意向排它鎖的一種笤闯。
在多事務(wù)同時(shí)寫入不同數(shù)據(jù)至同一索引間隙的時(shí)堕阔,不會(huì)發(fā)生鎖等待,事務(wù)之間互相不影響其他事務(wù)的完成颗味,這和間隙鎖的定義是一致的超陆。
假設(shè)一個(gè)記錄索引包含4和7,其他不同的事務(wù)分別插入5和6浦马,此時(shí)只要行不沖突时呀,插入意向鎖不會(huì)互相等待,可以直接獲取晶默。參照鎖兼容/沖突矩陣谨娜。
插入意向鎖的例子不再列舉,可以查看gap鎖的第一個(gè)例子磺陡。
3.6 自增鎖
自增鎖(AUTO-INC Locks)是事務(wù)插入時(shí)自增列上特殊的表級(jí)別的鎖趴梢。最簡(jiǎn)單的一種情況:如果一個(gè)事務(wù)正在向表中插入值漠畜,則任何其他事務(wù)必須等待,以便第一個(gè)事務(wù)插入的行接收連續(xù)的主鍵值垢油。
我們一般把主鍵設(shè)置為AUTO_INCREMENT
的列盆驹,默認(rèn)情況下這個(gè)字段的值為0,InnoDB會(huì)在AUTO_INCREMENT
修飾下的數(shù)據(jù)列所關(guān)聯(lián)的索引末尾設(shè)置獨(dú)占鎖滩愁。在訪問自增計(jì)數(shù)器時(shí)躯喇,InnoDB使用自增鎖,但是鎖定僅僅持續(xù)到當(dāng)前SQL語(yǔ)句的末尾硝枉,而不是整個(gè)事務(wù)的結(jié)束廉丽,畢竟自增鎖是表級(jí)別的鎖,如果長(zhǎng)期鎖定會(huì)大大降低數(shù)據(jù)庫(kù)的性能妻味。由于是表鎖正压,在使用期間,其他會(huì)話無法插入表中责球。
4 幻讀
這一章節(jié)焦履,我們通過幻讀,逐步展開對(duì)InnoDB鎖的探究雏逾。
4.1 幻讀概念
解釋了不同概念的鎖的作用域嘉裤,我們來看一下幻讀到底是什么∑懿幻讀在RR條件下是不會(huì)出現(xiàn)的屑宠。因?yàn)镽R是Repeatable Read,它是一種事務(wù)的隔離級(jí)別仇让,直譯過來也就是“在同一個(gè)事務(wù)中典奉,同樣的查詢語(yǔ)句的讀取是可重復(fù)”,也就是說他不會(huì)讀到”幻影行”(其他事務(wù)已經(jīng)提交的變更)丧叽,它讀到的只能是重復(fù)的(無論在第一次查詢之后其他事務(wù)做了什么操作卫玖,第二次查詢結(jié)果與第一次相同)。
上面的例子都是使用for update
蠢正,這種讀取操作叫做當(dāng)前讀骇笔,對(duì)于普通的select
語(yǔ)句均為快照讀。
當(dāng)前讀嚣崭,又叫加鎖讀,或者 阻塞讀懦傍。這種讀取操作不再是讀取快照雹舀,而是讀取最新版本并且加鎖。
快照讀不會(huì)添加任何鎖粗俱。
官方文檔對(duì)于幻讀的定義是這樣的:
原文:The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
手動(dòng)無腦翻譯:所謂的幻影行問題是指说榆,在同一個(gè)事務(wù)中,同樣的查詢語(yǔ)句執(zhí)行多次,得到了不同的結(jié)果签财,這就是幻讀串慰。例如,如果同一個(gè)SELECT
語(yǔ)句執(zhí)行了兩次唱蒸,第二次執(zhí)行的時(shí)候比第一次執(zhí)行時(shí)多出一行邦鲫,則該行就是所謂的幻影行。
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times.
神汹,這句話看起來應(yīng)該是不可重復(fù)讀的定義庆捺,同樣的查詢得到了不同的結(jié)果(兩次結(jié)果不是重復(fù)的),但是后面的舉例給出了幻讀真正的定義屁魏,第二次比第一次多出了一行滔以。也就是說,幻讀的出現(xiàn)有這樣一個(gè)前提氓拼,第二次查詢前其他事務(wù)提交了一個(gè)INSERT
插入語(yǔ)句你画。而不可重復(fù)讀出現(xiàn)的前提是第二次查詢前其他事務(wù)提交了UPDATE
或者DELETE
操作。
mysql的快照讀桃漾,使得在RR的隔離級(jí)別上在next-Key的作用區(qū)間內(nèi)坏匪,制造了一個(gè)快照副本,這個(gè)副本是隔離的呈队,無論副本對(duì)應(yīng)的區(qū)間里的數(shù)據(jù)被其他事務(wù)如何修改剥槐,在當(dāng)前事務(wù)中,取到的數(shù)據(jù)永遠(yuǎn)是副本中的數(shù)據(jù)宪摧。
RR級(jí)別下之所以可以讀到之前版本的數(shù)據(jù)询枚,是由于數(shù)據(jù)庫(kù)的MVCC(Multi-Version Concurrency Control,多版本并發(fā)控制)击狮。參見InnoDB Multi-Versioning
有些文章中提到“RR也不能完全避免幻讀”筹陵,實(shí)際上官方文檔實(shí)際要表達(dá)的意義是“在同一個(gè)事務(wù)內(nèi),多次連續(xù)查詢的結(jié)果是一樣的沿彭,不會(huì)因其他事務(wù)的修改而導(dǎo)致不同的查詢結(jié)果”朽砰,這里先給出實(shí)驗(yàn)結(jié)論:
1.當(dāng)前事務(wù)如果未發(fā)生更新操作(增刪改),快照版本會(huì)保持不變喉刘,多次查詢讀取的副本是同一個(gè)瞧柔。
2.當(dāng)前事務(wù)如果發(fā)生更新(增刪改),再次查詢時(shí)睦裳,會(huì)刷新快照版本造锅。
4.2 RC級(jí)別下的幻讀
RC情況下會(huì)出現(xiàn)幻讀。
首先設(shè)置隔離級(jí)別為RC,SET SESSION tx_isolation='READ-COMMITTED';
事務(wù)一 | 事務(wù)二 |
---|---|
mysql> SET SESSION tx_isolation='READ-COMMITTED'; mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(9,9); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 9 | 9 | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
RC(Read Commit)隔離級(jí)別可以避免臟讀廉邑,事務(wù)內(nèi)無法獲取其他事務(wù)未提交的變更哥蔚,但是由于能夠讀到已經(jīng)提交的事務(wù)倒谷,因此會(huì)出現(xiàn)幻讀和不重復(fù)讀。
也就是說糙箍,RC的快照讀是讀取最新版本數(shù)據(jù)渤愁,而RR的快照讀是讀取被next-key鎖作用區(qū)域的副本
4.3 RR級(jí)別下能否避免幻讀?
我們先來模擬一下RR隔離級(jí)別下沒有出現(xiàn)幻讀的情況:
開啟第一個(gè)事務(wù)并執(zhí)行一次快照查詢深夯。
事務(wù)一 | 事務(wù)二 |
---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(9,9); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
這兩個(gè)事務(wù)的執(zhí)行抖格,有兩個(gè)問題:
1.為什么之前的例子中,在第二個(gè)事務(wù)的INSERT
被阻塞了塌西,而這次卻執(zhí)行成功了他挎。
這是因?yàn)樵瓉淼恼Z(yǔ)句中帶有for update
,這種讀取是當(dāng)前讀捡需,會(huì)加鎖办桨。而本次第一個(gè)事務(wù)中的SELECT
僅僅是快照讀,沒有加任何鎖站辉。所以不會(huì)阻塞其他的插入呢撞。
2.數(shù)據(jù)庫(kù)中的數(shù)據(jù)已經(jīng)改變,為什么會(huì)讀不到饰剥?
這個(gè)就是之前提到的next-key lock鎖定的副本殊霞。RC及以下級(jí)別才會(huì)讀到已經(jīng)提交的事務(wù)。更多的業(yè)務(wù)邏輯是希望在某段時(shí)間內(nèi)或者某個(gè)特定的邏輯區(qū)間中汰蓉,前后查詢到的數(shù)據(jù)是一致的绷蹲,當(dāng)前事務(wù)是和其他事務(wù)隔離的。這也是數(shù)據(jù)庫(kù)在設(shè)計(jì)實(shí)現(xiàn)時(shí)遵循的ACID原則顾孽。
再給出RR條件下出現(xiàn)幻讀的情形祝钢,這種情形不需要兩個(gè)事務(wù),一個(gè)事務(wù)就已經(jīng)可以說明若厚,
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where id>8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.00 sec)
mysql> update test set code=9 where id=10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from test where id>8;
+----+------+
| id | code |
+----+------+
| 10 | 9 |
+----+------+
1 row in set (0.00 sec)
至于RR隔離級(jí)別下到底會(huì)不會(huì)出現(xiàn)幻讀拦英,就需要看幻讀的定義中的查詢到底是連續(xù)的查詢還是不連續(xù)的查詢。如果認(rèn)為RR級(jí)別下可能會(huì)出現(xiàn)幻讀测秸,那該級(jí)別下也會(huì)出現(xiàn)不重復(fù)讀疤估。
RR隔離級(jí)別下,雖然不會(huì)出現(xiàn)幻讀霎冯,但是會(huì)因此產(chǎn)生其他的問題铃拇。
前提:當(dāng)前數(shù)據(jù)表中只存在(1,1),(5,5),(10,10)三組數(shù)據(jù)。
如果數(shù)據(jù)庫(kù)隔離級(jí)別不是默認(rèn)沈撞,可以執(zhí)行SET SESSION tx_isolation='REPEATABLE-READ';
(該語(yǔ)句不是全局設(shè)置)更新為RR锚贱。
然后執(zhí)行下列操作:
事務(wù)一 | 事務(wù)二 | 備注 |
---|---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
開啟事務(wù)一,并查詢code>8 的記錄关串,只有一條(10,10) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(11,11); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) |
開啟第二個(gè)事務(wù)拧廊,插入(11,11)并提交 | |
mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
事務(wù)一再查詢一次,由于RR級(jí)別并沒有讀到更新 | |
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values(11,11); ERROR 1062 (23000): Duplicate entry '11' for key 'PRIMARY' |
事務(wù)一明明沒有查到晋修,卻插入不了 |
4.5 更新丟失(Lost Update)
4.5.1 更新丟失
除了上述這類問題外吧碾,RR還會(huì)有丟失更新的問題。
如下表給出的操作:
事務(wù)一 | 事務(wù)二 | 備注 |
---|---|---|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
開啟事務(wù)一墓卦,并查詢code>8 的記錄倦春,只有一條(10,10) |
|
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> update test set id=12,code=12 where id=10; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec) |
開啟第二個(gè)事務(wù),將(10,10)改為(12,12)并提交落剪,注意這里matched是1睁本,changed也是1 | |
mysql> select * from test where code > 8; +----+------+ | id | code | +----+------+ | 10 | 10 | +----+------+ 1 row in set (0.01 sec) |
事務(wù)一再次查詢code>8 的記錄,仍然只有一條(10,10) |
|
mysql> update test set id=9,code=9 where id=10; Query OK, 0 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 |
這里查詢到0條忠怖,更新了0條 |
這個(gè)例子里呢堰,事務(wù)一的更新是無效的,盡管在這個(gè)事務(wù)里程序認(rèn)為還存在(10,10)記錄凡泣。
事務(wù)一中更新之前的SELECT
操作是快照讀枉疼,所以讀到了快照里的(10,10),而UPDATE
中的WHERE
子句是當(dāng)前讀鞋拟,取得是最新版本的數(shù)據(jù)骂维,所以matched: 0 Changed: 0
。
如果上述例子中的操作是對(duì)同一條記錄做修改贺纲,就會(huì)引起更新丟失航闺。例如,事務(wù)一和二同時(shí)開啟猴誊,事務(wù)一先執(zhí)行update test set code=100 where id=10;
潦刃,事務(wù)二再執(zhí)行update test set code=200 where id=10;
,事務(wù)一的更新就會(huì)被覆蓋稠肘。
這就是經(jīng)典的丟失更新問題福铅,英文叫
Lost Update
,又叫提交覆蓋项阴,因?yàn)槭亲詈髨?zhí)行更新的事務(wù)提交導(dǎo)致的覆蓋滑黔。還有一種更新丟失叫做回滾覆蓋,即一個(gè)事務(wù)的回滾把另一個(gè)事務(wù)提交的數(shù)據(jù)給回滾覆蓋了环揽,但是目前市面上所有的數(shù)據(jù)庫(kù)都不支持這種stupid的操作略荡,因此不再詳述。
4.5.2 樂觀鎖與悲觀鎖
這種情況下歉胶,引入我們常見的兩種方式來解決該問題
-
樂觀鎖:在
UPDATE
的WHERE
子句中加入版本號(hào)信息來確定修改是否生效 -
悲觀鎖:在
UPDATE
執(zhí)行前汛兜,SELECT
后面加上FOR UPDATE
來給記錄加鎖,保證記錄在UPDATE
前不被修改通今。SELECT ... FOR UPDATE
是加上了X鎖粥谬,也可以通過SELECT ... LOCK IN SHARE MODE
加上S鎖肛根,來防止其他事務(wù)對(duì)該行的修改。
無論是樂觀鎖還是悲觀鎖漏策,使用的思想都是一致的派哲,那就是當(dāng)前讀。樂觀鎖利用當(dāng)前讀
判斷是否是最新版本掺喻,悲觀鎖利用當(dāng)前讀
鎖定行芭届。
但是使用樂觀鎖時(shí)仍然需要非常謹(jǐn)慎,因?yàn)镽R是可重復(fù)讀的感耙,一定不能在UPDATE之前先把版本號(hào)讀取出來褂乍。
5. InnoDB對(duì)不同語(yǔ)句執(zhí)行時(shí)的加鎖狀況
如果一個(gè)SQL語(yǔ)句要對(duì)二級(jí)索引(非主鍵索引)設(shè)置X模式的Record鎖,InnoDB還會(huì)檢索出相應(yīng)的聚簇索引(主鍵索引)并對(duì)它們?cè)O(shè)置鎖定即硼。
5.1 SELECT ... FROM...不加鎖
SELECT ... FROM
是快照讀取逃片,除了SERIALIZABLE
的事務(wù)隔離級(jí)別,該SQL語(yǔ)句執(zhí)行時(shí)不會(huì)加任何鎖谦絮。
SERIALIZABLE
級(jí)別下题诵,SELECT
語(yǔ)句的執(zhí)行會(huì)在遇到的索引記錄上設(shè)置S模式的next-key鎖。但是對(duì)于唯一索引层皱,只鎖定索引記錄性锭,而不會(huì)鎖定gap。
5.2 UPDATE系列
S鎖讀取(SELECT ... LOCK IN SHARE MODE
)叫胖,X鎖讀取(SELECT ... FOR UPDATE
)草冈、更新UPDATE
和刪除DELETE
這四類語(yǔ)句,采用的鎖取決于搜索條件中使用的索引類型瓮增。
- 如果使用唯一索引怎棱,InnoDB僅鎖定索引記錄本身,不鎖定間隙绷跑。
- 如果使用非唯一索引拳恋,或者未命中索引,InnoDB使用間隙鎖或者next-key鎖來鎖定索引范圍砸捏,這樣就可以阻止其他事務(wù)插入鎖定范圍谬运。
5.2.1 UPDATE語(yǔ)句
UPDATE ... WHERE ...
在搜索遇到的每條記錄上設(shè)置一個(gè)獨(dú)占的next-key鎖,如果是唯一索引只鎖定記錄垦藏。
當(dāng)UPDATE
修改聚簇索引時(shí)梆暖,將對(duì)受影響的二級(jí)索引采用隱式鎖,隱式鎖是在索引中對(duì)二級(jí)索引的記錄邏輯加鎖掂骏,實(shí)際上不產(chǎn)生鎖對(duì)象轰驳,不占用內(nèi)存空間。
例如
update test set code=100 where id=10;
執(zhí)行的時(shí)候code=10
的索引(code是二級(jí)索引,見文中給出的建表語(yǔ)句)會(huì)被加隱式鎖级解,只有隱式鎖產(chǎn)生沖突時(shí)才會(huì)變成顯式鎖(如S鎖冒黑、X鎖)。即此時(shí)另一個(gè)事務(wù)也去更新id=10
這條記錄蠕趁,隱式鎖就會(huì)升級(jí)為顯示鎖薛闪。
這樣做的好處是降低了鎖的開銷。
UPDATE
可能會(huì)導(dǎo)致新的普通索引的插入俺陋。當(dāng)新的索引插入之前,會(huì)首先執(zhí)行一次重復(fù)索引檢查昙篙。在重復(fù)檢查和插入時(shí)腊状,更新操作會(huì)對(duì)受影響的二級(jí)索引記錄采用共享鎖定(S鎖)。
5.2.2 DELETE語(yǔ)句
DELETE FROM ... WHERE ...
在搜索遇到的每條記錄上設(shè)置一個(gè)獨(dú)占的next-key鎖,如果是唯一索引只鎖定記錄苔可。
5.3 INSERT
INSERT
區(qū)別于UPDATE系列單獨(dú)列出缴挖,是因?yàn)樗奶幚矸绞捷^為特別。
插入行之前焚辅,會(huì)設(shè)置一種插入意向鎖映屋,插入意向鎖表示插入的意圖。如果其它事務(wù)在要插入的位置上設(shè)置了X鎖同蜻,則無法獲取插入意向鎖棚点,插入操作也因此阻塞。
INSERT
在插入的行上設(shè)置X鎖湾蔓。該鎖是一個(gè)Record鎖瘫析,并不是next-key鎖,即只鎖定記錄本身默责,不鎖定間隙贬循,因此不會(huì)阻止其他會(huì)話在這行記錄前的間隙中插入新的記錄。
具體的加鎖過程桃序,見6.2杖虾。
6. 可能的死鎖場(chǎng)景
6.1 Duplicate key error引發(fā)的死鎖
并發(fā)條件下,唯一鍵索引沖突可能會(huì)導(dǎo)致死鎖媒熊,這種死鎖一般分為兩種奇适,一種是rollback
引發(fā),另一種是commit
引發(fā)泛释。
6.1.1 rollback
引發(fā)的Duplicate key死鎖
我命名為insert-insert-insert-rollback死鎖
事務(wù)一 | 事務(wù)二 | 事務(wù)三 |
---|---|---|
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); Query OK, 1 row affected (0.01 sec) |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執(zhí)行之后被阻塞滤愕,等待事務(wù)一 |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執(zhí)行之后被阻塞,等待事務(wù)一 |
||
mysql>rollback; Query OK, 0 rows affected (0.00 sec) |
||
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | ||
Query OK, 1 row affected (16.13 sec) |
當(dāng)事務(wù)一執(zhí)行回滾時(shí)怜校,事務(wù)二和事務(wù)三發(fā)生了死鎖间影。InnoDB的死鎖檢測(cè)一旦檢測(cè)到死鎖發(fā)生,會(huì)自動(dòng)失敗其中一個(gè)事務(wù)茄茁,因此看到的結(jié)果是一個(gè)失敗另一個(gè)成功魂贬。
為什么會(huì)死鎖巩割?
死鎖產(chǎn)生的原因是事務(wù)一插入記錄時(shí),對(duì)(2,2)記錄加X鎖付燥,此時(shí)事務(wù)二和事務(wù)三插入數(shù)據(jù)時(shí)檢測(cè)到了重復(fù)鍵錯(cuò)誤宣谈,此時(shí)事務(wù)二和事務(wù)三要在這條索引記錄上設(shè)置S鎖,由于X鎖的存在键科,S鎖的獲取被阻塞闻丑。
事務(wù)一回滾,由于S鎖和S鎖是可以兼容的勋颖,因此事務(wù)二和事務(wù)三都獲得了這條記錄的S鎖嗦嗡,此時(shí)其中一個(gè)事務(wù)希望插入,則該事務(wù)期望在這條記錄上加上X鎖饭玲,然而另一個(gè)事務(wù)持有S鎖侥祭,S鎖和X鎖互相是不兼容的,兩個(gè)事務(wù)就開始互相等待對(duì)方的鎖釋放茄厘,造成了死鎖矮冬。
事務(wù)二和事務(wù)三為什么會(huì)加S鎖,而不是直接等待X鎖
事務(wù)一的insert語(yǔ)句加的是隱式鎖(隱式的Record鎖次哈、X鎖)胎署,但是其他事務(wù)插入同一行記錄時(shí),出現(xiàn)了唯一鍵沖突亿乳,事務(wù)一的隱式鎖升級(jí)為顯示鎖硝拧。
事務(wù)二和事務(wù)三在插入之前判斷到了唯一鍵沖突,是因?yàn)椴迦肭暗?strong>重復(fù)索引檢查葛假,這次檢查必須進(jìn)行一次當(dāng)前讀障陶,于是非唯一索引就會(huì)被加上S模式的next-key鎖,唯一索引就被加上了S模式的Record鎖聊训。
因?yàn)椴迦牒透轮岸家M(jìn)行重復(fù)索引檢查而執(zhí)行當(dāng)前讀操作抱究,所以RR隔離級(jí)別下,同一個(gè)事務(wù)內(nèi)不連續(xù)的查詢带斑,可能也會(huì)出現(xiàn)幻讀的效果(但個(gè)人并不認(rèn)為RR級(jí)別下也會(huì)出現(xiàn)幻讀鼓寺,幻讀的定義應(yīng)該是連續(xù)的讀取)。而連續(xù)的查詢由于都是讀取快照勋磕,中間沒有當(dāng)前讀的操作妈候,所以不會(huì)出現(xiàn)幻讀。
6.1.2 commit
引發(fā)的Duplicate key死鎖
delete-insert-insert-commit死鎖
事務(wù)一 | 事務(wù)二 | 事務(wù)三 |
---|---|---|
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from test where id=2; Query OK, 1 row affected (0.01 sec) |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執(zhí)行之后被阻塞挂滓,等待事務(wù)一 |
||
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into test values (2,2); 執(zhí)行之后被阻塞苦银,等待事務(wù)一 |
||
mysql>commit; Query OK, 0 rows affected (0.00 sec) |
||
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | ||
Query OK, 1 row affected (2.37 sec) |
這種情況下產(chǎn)生的死鎖和insert-insert-insert-rollback
死鎖產(chǎn)生的原理一致。
6.2 數(shù)據(jù)插入的過程
經(jīng)過以上分析,一條數(shù)據(jù)在插入時(shí)經(jīng)過以下幾個(gè)過程:
假設(shè)數(shù)據(jù)表test.test
中存在(1,1)幔虏、(5,5)和(10,10)三條記錄纺念。
- 事務(wù)開啟,嘗試獲取插入意向鎖想括。例如陷谱,事務(wù)一執(zhí)行了
select * from test where id>8 for update
,事務(wù)二要插入(9,9)瑟蜈,此時(shí)先要獲取插入意向鎖烟逊,由于事務(wù)一已經(jīng)在對(duì)應(yīng)的記錄和間隙上加了X鎖,因此事務(wù)二被阻塞踪栋,并且阻塞的原因是獲取插入意向鎖時(shí)被事務(wù)一的X鎖阻塞焙格。 - 獲取意向鎖之后,插入之前進(jìn)行重復(fù)索引檢查夷都。重復(fù)索引檢查為當(dāng)前讀,需要添加S鎖予颤。
- 如果是已經(jīng)存在唯一索引囤官,且索引未加鎖。直接拋出
Duplicate key
的錯(cuò)誤蛤虐。如果存在唯一索引党饮,且索引加鎖,等待鎖釋放驳庭。 - 重復(fù)檢查通過之后刑顺,加入X鎖,插入記錄
6.3 GAP與Insert Intention沖突引發(fā)死鎖
update-insert死鎖
仍然是表test饲常,當(dāng)前表中的記錄如下:
mysql> select * from test;
+----+------+
| id | code |
+----+------+
| 1 | 1 |
| 5 | 5 |
| 10 | 10 |
+----+------+
3 rows in set (0.01 sec)
事務(wù)一 | 事務(wù)二 |
---|---|
begin; | begin; |
select * from test where id=5 for update; | select * from test where id=10 for update; |
insert into test values(7,7); | |
insert into test values(7,7); | |
Query OK, 1 row affected (5.03 sec) | |
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
使用show engine innodb status
查看死鎖狀態(tài)蹲堂。先后出現(xiàn)lock_mode X locks gap before rec insert intention waiting
和lock_mode X locks gap before rec
字眼,是gap鎖和插入意向鎖的沖突導(dǎo)致的死鎖贝淤。
回顧select...for update的加鎖范圍
首先回顧一下兩個(gè)事務(wù)中的select ... for update
做了哪些加鎖操作柒竞。
code=5
時(shí),首先會(huì)獲取code=5
的索引記錄鎖(Record鎖)播聪,根據(jù)之前gap鎖的介紹朽基,會(huì)在前一個(gè)索引和當(dāng)前索引之間的間隙加鎖,于是區(qū)間(1,5)之間被加上了X模式的gap鎖离陶。除此之外RR模式下稼虎,還會(huì)加next-key鎖,于是區(qū)間(5,10]被加了next-key鎖招刨;
- 因此霎俩,
code=5
的加鎖范圍是,區(qū)間(1,5)的gap鎖,{5}索引Record鎖茸苇,(5,10]的next-key鎖排苍。即區(qū)間(1,10)上都被加上了X模式的鎖。 - 同理学密,
code=10
的加鎖范圍是淘衙,區(qū)間(5,10)的gap鎖,{10}索引Record鎖,(10,+∞)的next-key鎖腻暮。
由gap鎖的特性彤守,兼容矩陣中沖突的鎖也可以被不同的事務(wù)同時(shí)加在一個(gè)間隙上。上述兩個(gè)select ... for update
語(yǔ)句出現(xiàn)了間隙鎖的交集哭靖,code=5
的next-key鎖和code=10
的gap鎖有重疊的區(qū)域——(5,10)具垫。
死鎖的成因
當(dāng)事務(wù)一執(zhí)行插入語(yǔ)句時(shí),會(huì)先加X模式的插入意向鎖
试幽,即兼容矩陣中的IX鎖筝蚕。
但是由于插入意向鎖要鎖定的位置存在X模式的gap鎖
。兼容矩陣中IX和X鎖是不兼容的铺坞,因此事務(wù)一的IX鎖會(huì)等待事務(wù)二的gap鎖釋放起宽。
事務(wù)二也執(zhí)行插入語(yǔ)句,與事務(wù)一同樣济榨,事務(wù)二的插入意向鎖IX鎖會(huì)等待事務(wù)一的gap鎖釋放坯沪。
兩個(gè)事務(wù)互相等待對(duì)方先釋放鎖,因此出現(xiàn)死鎖擒滑。
7 總結(jié)
除了以上給出的幾種死鎖模式腐晾,還有很多其他死鎖的場(chǎng)景。
無論是哪種場(chǎng)景丐一,萬變不離其宗藻糖,都是由于某個(gè)區(qū)間上或者某一個(gè)記錄上可以同時(shí)持有鎖,例如不同事務(wù)在同一個(gè)間隙gap上的鎖不沖突钝诚;不同事務(wù)中颖御,S鎖可以阻塞X鎖的獲取,但是不會(huì)阻塞另一個(gè)事務(wù)獲取該S鎖凝颇。這樣才會(huì)出現(xiàn)兩個(gè)事務(wù)同時(shí)持有鎖潘拱,并互相等待,最終導(dǎo)致死鎖拧略。
其中需要注意的點(diǎn)是芦岂,增、刪垫蛆、改的操作都會(huì)進(jìn)行一次當(dāng)前讀操作禽最,以此獲取最新版本的數(shù)據(jù)腺怯,并檢測(cè)是否有重復(fù)的索引。
這個(gè)過程除了會(huì)導(dǎo)致RR隔離級(jí)別下出現(xiàn)死鎖之外還會(huì)導(dǎo)致其他兩個(gè)問題:
- 第一個(gè)是可重復(fù)讀可能會(huì)因?yàn)檫@次的當(dāng)前讀操作而中斷川无,(同樣呛占,幻讀可能也會(huì)因此產(chǎn)生);
- 第二個(gè)是其他事務(wù)的更新可能會(huì)丟失(解決方式:悲觀鎖懦趋、樂觀鎖)晾虑。