轉(zhuǎn)自:https://www.cnblogs.com/AaronCui/p/10490663.html
意向鎖:https://blog.csdn.net/weixin_34278190/article/details/88039951
唯一鍵死鎖:https://www.cnblogs.com/AaronCui/p/10508831.html
1. 前言
數(shù)據(jù)事務(wù)設(shè)計遵循ACID的原則诬垂。
MySQL數(shù)據(jù)庫提供了四種默認(rèn)的隔離級別施禾,讀未提交(read-uncommitted)、讀已提交(或不可重復(fù)讀)(read-committed)、可重復(fù)讀(repeatable-read)漫蛔、串行化(serializable)搓蚪。
MySQL的默認(rèn)隔離級別是RR。
2. 鎖基本概念
2.1 共享鎖和排它鎖
InnoDB實(shí)現(xiàn)了兩種標(biāo)準(zhǔn)行級鎖豌骏,一種是共享鎖(shared locks代承,S鎖)汁蝶,另一種是獨(dú)占鎖,或者叫排它鎖(exclusive locks论悴,X鎖)掖棉。
S鎖允許當(dāng)前持有該鎖的事務(wù)讀取行。
X鎖允許當(dāng)前持有該鎖的事務(wù)更新或刪除行膀估。
S鎖
如果事務(wù)T1持有了行r上的S鎖幔亥,則其他事務(wù)可以同時持有行r的S鎖,但是不能對行r加X鎖玖像。
X鎖
如果事務(wù)T1持有了行r上的X鎖紫谷,則其他任何事務(wù)不能持有行r的X鎖,必須等待T1在行r上的X鎖釋放捐寥。
如果事務(wù)T1在行r上保持S鎖笤昨,則另一個事務(wù)T2對行r的鎖的請求按如下方式處理:
T2可以同時持有S鎖
T2如果想在行r上獲取X鎖,必須等待其他事務(wù)對該行添加的S鎖或X鎖的釋放握恳。
2.2 意向鎖-Intention Locks
InnoDB支持多種粒度的鎖瞒窒,允許行級鎖和表級鎖的共存。例如LOCK TABLES ... WRITE等語句可以在指定的表上加上獨(dú)占鎖乡洼。
InnoBD使用意向鎖來實(shí)現(xiàn)多個粒度級別的鎖定崇裁。意向鎖是表級鎖,表示table中的row所需要的鎖(S鎖或X鎖)的類型束昵。
意向鎖分為意向共享鎖(IS鎖)和意向排它鎖(IX鎖)拔稳。
IS鎖表示當(dāng)前事務(wù)意圖在表中的行上設(shè)置共享鎖,下面語句執(zhí)行時會首先獲取IS鎖锹雏,因?yàn)檫@個操作在獲取S鎖:
SELECT...LOCKINSHAREMODE
IX鎖表示當(dāng)前事務(wù)意圖在表中的行上設(shè)置排它鎖巴比。下面語句執(zhí)行時會首先獲取IX鎖,因?yàn)檫@個操作在獲取X鎖:
SELECT...FORUPDATE
事務(wù)要獲取某個表上的S鎖和X鎖之前礁遵,必須先分別獲取對應(yīng)的IS鎖和IX鎖轻绞。
2.3 鎖的兼容性
鎖的兼容矩陣如下:
---排它鎖(X)意向排它鎖(IX)共享鎖(S)意向共享鎖(IS)
排它鎖(X)NNNN
意向排它鎖(IX)NOKNOK
共享鎖(S)NNOKOK
意向共享鎖(IS)NOKOKOK
按照上面的兼容性,如果不同事務(wù)之間的鎖兼容佣耐,則當(dāng)前加鎖事務(wù)可以持有鎖政勃,如果有沖突則會等待其他事務(wù)的鎖釋放。
如果一個事務(wù)請求鎖時兼砖,請求的鎖與已經(jīng)持有的鎖沖突而無法獲取時奸远,互相等待就可能會產(chǎn)生死鎖既棺。
意向鎖不會阻止除了全表鎖定請求之外的任何鎖請求。
意向鎖的主要目的是顯示事務(wù)正在鎖定某行或者正意圖鎖定某行懒叛。
3. InnoDB中的鎖
常見的鎖有Record鎖援制、gap鎖、next-key鎖芍瑞、插入意向鎖、自增鎖等褐墅。
下面會對每一種鎖給出一個查看鎖的示例拆檬。
3.1 準(zhǔn)備工作
3.1.1 測試用表結(jié)構(gòu)
示例的基礎(chǔ)是一個只有兩列的數(shù)據(jù)庫表。
mysql> CREATE TABLE test (idint(11)NOTNULL,codeint(11)NOTNULL, PRIMARY KEY(id), KEY (code)) ENGINE=InnoDBDEFAULTCHARSET=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)證普通索引(非唯一鍵二級索引)時就使用code列。
3.1.2 查看鎖狀態(tài)的方式
要看到鎖的情況哗咆,必須手動開啟多個事務(wù)蜘欲,其中一些鎖的狀態(tài)的查看則必須使鎖處于waiting狀態(tài),這樣才能在mysql的引擎狀態(tài)日志中看到晌柬。
命令:
mysql> show engine innodb status;
這條命令能顯示最近幾個事務(wù)的狀態(tài)姥份、查詢和寫入情況等信息。當(dāng)出現(xiàn)死鎖時年碘,命令能給出最近的死鎖明細(xì)澈歉。
3.2 記錄鎖 Record Locks
Record鎖
Record Lock是對索引記錄的鎖定。記錄鎖有兩種模式屿衅,S模式和X模式埃难。
例如SELECT id FROM test WHERE id = 10 FOR UPDATE;表示防止任何其他事務(wù)插入、更新或者刪除id =10的行傲诵。
記錄鎖始終只鎖定索引凯砍。即使表沒有建立索引,InnoDB也會創(chuàng)建一個隱藏的聚簇索引(隱藏的遞增主鍵索引)拴竹,并使用此索引進(jìn)行記錄鎖定悟衩。
查看記錄鎖
開啟第一個事務(wù),不提交栓拜,測試完之后回滾座泳。
mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql>updatetestsetid=2whereid=1;Query OK, 1 row affected (0.00 sec)Rows matched: 1? Changed: 1? Warnings: 0
事務(wù)加鎖情況
mysql> show engine innodbstatus\G;...------------TRANSACTIONS---------------TRANSACTION 366811, ACTIVE 690 sec2lock struct(s), heap size1136,1row lock(s), undologentries2MySQL thread id785, OS thread handle123145432457216, query id729076localhost127.0.0.1root...
可以看到有一行被加了鎖惠昔。由之前對鎖的描述可以推測出,update語句給id=1這一行上加了一個X鎖挑势。
注意:X鎖廣義上是一種抽象意義的排它鎖镇防,即鎖一般分為X模式和S模式,狹義上指row或者index上的鎖潮饱,而Record鎖是索引上的鎖来氧。
為了不修改數(shù)據(jù),可以用select ... for update語句香拉,加鎖行為和update啦扬、delete是一樣的,insert加鎖機(jī)制較為復(fù)雜凫碌,后面的章節(jié)會提到扑毡。
第一個事務(wù)保持原狀,不要提交或者回滾盛险,現(xiàn)在開啟第二個事務(wù)瞄摊。
mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql>updatetestsetid=3whereid=1;
執(zhí)行update時,sql語句的執(zhí)行被阻塞了苦掘。查看下事務(wù)狀態(tài):
mysql> show engine innodb status\G;...------- TRX HAS BEEN WAITING4SECFORTHIS LOCKTOBE GRANTED:RECORD LOCKSspaceid62page no3n bits72index PRIMARY of table `test`.`test` trx id366820lock_mode X locks rec butnotgap waitingRecord lock, heap no2PHYSICAL RECORD: n_fields3; compact format; info bits320:len8;hex0000000000000001;asc;;1:len6;hex0000000598e3;asc;;2:len7;hex7e000001a80896;asc~? ? ? ;;------------------...
喜聞樂見换帜,我們看到了這個鎖的狀態(tài)。狀態(tài)標(biāo)題是'事務(wù)正在等待獲取鎖'鸟蜡,描述中的lock_mode X locks rec but not gap就是本章節(jié)中的record記錄鎖膜赃,直譯一下'X鎖模式鎖住了記錄'。后面還有一句but not gap意思是只對record本身加鎖揉忘,并不對間隙加鎖跳座,間隙鎖的敘述見下一個章節(jié)。
3.3 間隙鎖 Gap Locks
間隙鎖
間隙鎖作用在索引記錄之間的間隔泣矛,又或者作用在第一個索引之前疲眷,最后一個索引之后的間隙。不包括索引本身您朽。
例如狂丝,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;這條語句阻止其他事務(wù)插入10和20之間的數(shù)字,無論這個數(shù)字是否存在哗总。
間隙可以跨越0個几颜,單個或多個索引值。
間隙鎖是性能和并發(fā)權(quán)衡的產(chǎn)物讯屈,只存在于部分事務(wù)隔離級別蛋哭。
select * from table where id=1;
唯一索引可以鎖定一行,所以不需要間隙鎖鎖定涮母。
如果列沒有索引或者具有非唯一索引谆趾,該語句會鎖定當(dāng)前索引前的間隙躁愿。
在同一個間隙上,不同的事務(wù)可以持有上述兼容/沖突表中沖突的兩個鎖沪蓬。例如彤钟,事務(wù)T1現(xiàn)在持有一個間隙S鎖,T2可以同時在同一個間隙上持有間隙X鎖跷叉。
允許沖突的鎖在間隙上鎖定的原因是逸雹,如果從索引中清除一條記錄,則由不同事務(wù)在這條索引記錄上的加間隙鎖的動作必須被合并云挟。
InnoDB中的間隙鎖的唯一目的是防止其他事務(wù)插入間隙峡眶。
間隙鎖是可以共存的,一個事務(wù)占用的間隙鎖不會阻止另一個事務(wù)獲取同一個間隙上的間隙鎖植锉。
如果事務(wù)隔離級別改為RC,則間隙鎖會被禁用峭拘。
查看間隙鎖
按照官方文檔俊庇,where子句查詢條件是唯一鍵且指定了值時,只有record鎖鸡挠,沒有g(shù)ap鎖辉饱。
如果where語句指定了范圍,gap鎖是存在的拣展。
這里只測試驗(yàn)證一下當(dāng)指定非唯一鍵索引的時候彭沼,gap鎖的位置,按照文檔的說法备埃,會鎖定當(dāng)前索引及索引之前的間隙姓惑。(指定了非唯一鍵索引,例如code=10,間隙鎖仍然存在)
開啟第一個事務(wù)按脚,鎖定一條非唯一的普通索引記錄
mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql>select * fromtestwherecode = 10forupdate;+----+------+| id | code |+----+------+| 10 |? 10 |+----+------+1 row in set (0.00 sec)
由于預(yù)存了兩條數(shù)據(jù)于毙,row(1,1)和row(10,10),此時這個間隙應(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;會鎖定code=10這個索引介蛉,并且會鎖定code<10的間隙。
開啟第二個事務(wù)溶褪,在code=10之前的間隙中插入一條數(shù)據(jù)币旧,看下這條數(shù)據(jù)是否能夠插入。
mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql>insert intotestvalues(2,2);
插入的時候竿滨,執(zhí)行被阻塞佳恬,查看引擎狀態(tài):
mysql> show engine innodbstatus\G;...---TRANSACTION 366864, ACTIVE 5 sec insertingmysql tablesinuse1, locked1LOCK WAIT2lock struct(s), heap size1136,1row lock(s), undologentries1MySQL thread id793, OS thread handle123145434963968, query id730065localhost127.0.0.1root updateinsertinto test values(2,2)------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id63page no4n bits72index code oftable`test`.`test` trx id366864lock_mode X locks gap before recinsertintention waitingRecord lock, heap no3PHYSICAL RECORD: n_fields2; compactformat; info bits00:len8; hex800000000000000a; asc? ? ? ? ;;1:len8; hex000000000000000a; asc? ? ? ? ;;------------------...
插入語句被阻塞了捏境,lock_mode X locks gap before rec,由于第一個事務(wù)鎖住了1到10之間的gap毁葱,需要等待獲取鎖之后才能插入垫言。
如果再開啟一個事務(wù),插入(0,0)
mysql> start transaction;mysql>insert intotestvalues(0,0);Query OK, 1 row affected (0.00 sec)
可以看到:指定的非唯一建索引的gap鎖的邊界是當(dāng)前索引到上一個索引之間的gap倾剿。
最后給出鎖定區(qū)間的示例,首先插入一條記錄(5,5)
mysql>insert intotestvalues(5,5);Query OK, 1 row affected (0.00 sec)
開啟第一個事務(wù):
mysql> start transaction;Query OK,0rows affected (0.00sec)mysql> select * from test where code between1and10forupdate;+----+------+| id |code|
+----+------+
|1|? ? 1 ||? 5 |5|
|10|? 10 |+----+------+3rowsinset (0.00sec)
第二個事務(wù)筷频,試圖去更新code=5的行:
mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql>updatetestsetcode=4wherecode=5;
執(zhí)行到這里,如果第一個事務(wù)不提交或者回滾的話前痘,第二個事務(wù)一直等待直至mysql中設(shè)定的超時時間凛捏。
3.4 Next-key Locks
Next-key鎖
Next-key鎖實(shí)際上是Record鎖和gap鎖的組合。Next-key鎖是在下一個索引記錄本身和索引之前的gap加上S鎖或是X鎖(如果是讀就加上S鎖芹缔,如果是寫就加X鎖)坯癣。
默認(rèn)情況下,InnoDB的事務(wù)隔離級別為RR最欠,系統(tǒng)參數(shù)innodb_locks_unsafe_for_binlog的值為false示罗。InnoDB使用next-key鎖對索引進(jìn)行掃描和搜索,這樣就讀取不到幻象行芝硬,避免了幻讀的發(fā)生蚜点。
幻讀是指在同一事務(wù)下,連續(xù)執(zhí)行兩次同樣的SQL語句拌阴,第二次的SQL語句可能會返回之前不存在的行绍绘。
當(dāng)查詢的索引是唯一索引時,Next-key lock會進(jìn)行優(yōu)化迟赃,降級為Record Lock陪拘,此時Next-key lock僅僅作用在索引本身,而不會作用于gap和下一個索引上纤壁。
查看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 |+----+------+3rowsinset (0.00sec)
由于id是主鍵悠反、唯一索引,mysql會做優(yōu)化馍佑,因此使用code這個非唯一鍵的二級索引來舉例說明斋否。
對于code,可能的next-key鎖的范圍是:
(-∞,1](1,5](5,10](10,+∞)
開啟第一個事務(wù)拭荤,在code=5的索引上請求更新:
mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql>select * fromtestwherecode=5forupdate;+----+------+| id | code |+----+------+|? 5 |? ? 5 |+----+------+1 row in set (8.81 sec)
之前在gap鎖的章節(jié)中介紹了茵臭,code=5 for update會在code=5的索引上加一個record鎖,還會在1
mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql>insert intotestvalues(8,8);
insert處于等待執(zhí)行的狀態(tài)舅世,這就是next-key鎖生效而導(dǎo)致的結(jié)果。第一個事務(wù),鎖定了區(qū)間(1,5]巍膘,由于RR的隔離級別下next-key鎖處于開啟生效狀態(tài),又鎖定了(5,10]區(qū)間摩钙。所以插入SQL語句的執(zhí)行被阻塞。
解釋:在這種情況下查辩,被鎖定的區(qū)域是code=5前一個索引到它的間隙胖笛,以及next-key的區(qū)域。code=5 for update對索引的鎖定用區(qū)間表示宜岛,gap鎖鎖定了(1,5)长踊,record鎖鎖定了{(lán)5}索引記錄,next-key鎖鎖住了(5,10]萍倡,也就是說整個(1,10]的區(qū)間被鎖定了身弊。由于是for update,所以這里的鎖都是X鎖列敲,因此阻止了其他事務(wù)中帶有沖突鎖定的操作執(zhí)行佑刷。
如果我們在第一個事務(wù)中,執(zhí)行了code>8 for update酿炸,在掃描過程中,找到了code=10涨冀,此時就會鎖住10之前的間隙(5到10之間的gap)填硕,10本身(record),和10之后的間隙(next-key)鹿鳖。此時另一個事務(wù)插入(6,6),(9,9)和(11,11)都是不被允許的扁眯,只有在前一個索引5及5之前的索引和間隙才能執(zhí)行插入(更新和刪除也會被阻塞)。
3.5 插入意向鎖 Insert Intention Locks
插入意向鎖在行插入之前由INSERT設(shè)置一種間隙鎖翅帜,是意向排它鎖的一種姻檀。
在多事務(wù)同時寫入不同數(shù)據(jù)至同一索引間隙的時,不會發(fā)生鎖等待涝滴,事務(wù)之間互相不影響其他事務(wù)的完成绣版,這和間隙鎖的定義是一致的。
假設(shè)一個記錄索引包含4和7歼疮,其他不同的事務(wù)分別插入5和6杂抽,此時只要行不沖突,插入意向鎖不會互相等待韩脏,可以直接獲取缩麸。參照鎖兼容/沖突矩陣。
插入意向鎖的例子不再列舉赡矢,可以查看gap鎖的第一個例子杭朱。
3.6 自增鎖
自增鎖(AUTO-INC Locks)是事務(wù)插入時自增列上特殊的表級別的鎖阅仔。最簡單的一種情況:如果一個事務(wù)正在向表中插入值,則任何其他事務(wù)必須等待弧械,以便第一個事務(wù)插入的行接收連續(xù)的主鍵值八酒。
我們一般把主鍵設(shè)置為AUTO_INCREMENT的列,默認(rèn)情況下這個字段的值為0梦谜,InnoDB會在AUTO_INCREMENT修飾下的數(shù)據(jù)列所關(guān)聯(lián)的索引末尾設(shè)置獨(dú)占鎖丘跌。在訪問自增計數(shù)器時,InnoDB使用自增鎖唁桩,但是鎖定僅僅持續(xù)到當(dāng)前SQL語句的末尾闭树,而不是整個事務(wù)的結(jié)束,畢竟自增鎖是表級別的鎖荒澡,如果長期鎖定會大大降低數(shù)據(jù)庫的性能报辱。由于是表鎖,在使用期間单山,其他會話無法插入表中碍现。
4 幻讀
這一章節(jié),我們通過幻讀米奸,逐步展開對InnoDB鎖的探究昼接。
4.1 幻讀概念
解釋了不同概念的鎖的作用域,我們來看一下幻讀到底是什么悴晰÷幻讀在RR條件下是不會出現(xiàn)的。因?yàn)镽R是Repeatable Read铡溪,它是一種事務(wù)的隔離級別漂辐,直譯過來也就是“在同一個事務(wù)中,同樣的查詢語句的讀取是可重復(fù)”棕硫,也就是說他不會讀到”幻影行”(其他事務(wù)已經(jīng)提交的變更)髓涯,它讀到的只能是重復(fù)的(無論在第一次查詢之后其他事務(wù)做了什么操作,第二次查詢結(jié)果與第一次相同)哈扮。
上面的例子都是使用for update纬纪,這種讀取操作叫做當(dāng)前讀,對于普通的select語句均為快照讀滑肉。
當(dāng)前讀育八,又叫加鎖讀,或者 阻塞讀赦邻。這種讀取操作不再是讀取快照髓棋,而是讀取最新版本并且加鎖。
快照讀不會添加任何鎖。
官方文檔對于幻讀的定義是這樣的:
原文: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.
手動無腦翻譯:所謂的幻影行問題是指按声,在同一個事務(wù)中膳犹,同樣的查詢語句執(zhí)行多次,得到了不同的結(jié)果签则,這就是幻讀须床。例如,如果同一個SELECT語句執(zhí)行了兩次渐裂,第二次執(zhí)行的時候比第一次執(zhí)行時多出一行豺旬,則該行就是所謂的幻影行。
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)有這樣一個前提蔬咬,第二次查詢前其他事務(wù)提交了一個INSERT插入語句鲤遥。而不可重復(fù)讀出現(xiàn)的前提是第二次查詢前其他事務(wù)提交了UPDATE或者DELETE操作。
mysql的快照讀林艘,使得在RR的隔離級別上在next-Key的作用區(qū)間內(nèi)盖奈,制造了一個快照副本,這個副本是隔離的狐援,無論副本對應(yīng)的區(qū)間里的數(shù)據(jù)被其他事務(wù)如何修改钢坦,在當(dāng)前事務(wù)中,取到的數(shù)據(jù)永遠(yuǎn)是副本中的數(shù)據(jù)咕村。
RR級別下之所以可以讀到之前版本的數(shù)據(jù),是由于數(shù)據(jù)庫的MVCC(Multi-Version Concurrency Control蚊俺,多版本并發(fā)控制)懈涛。參見InnoDB Multi-Versioning
有些文章中提到“RR也不能完全避免幻讀”,實(shí)際上官方文檔實(shí)際要表達(dá)的意義是“在同一個事務(wù)內(nèi)泳猬,多次連續(xù)查詢的結(jié)果是一樣的批钠,不會因其他事務(wù)的修改而導(dǎo)致不同的查詢結(jié)果”,這里先給出實(shí)驗(yàn)結(jié)論:
1.當(dāng)前事務(wù)如果未發(fā)生更新操作(增刪改)得封,快照版本會保持不變埋心,多次查詢讀取的副本是同一個。
2.當(dāng)前事務(wù)如果發(fā)生更新(增刪改)忙上,再次查詢時拷呆,會刷新快照版本。
4.2 RC級別下的幻讀
RC情況下會出現(xiàn)幻讀。
首先設(shè)置隔離級別為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)隔離級別可以避免臟讀茬斧,事務(wù)內(nèi)無法獲取其他事務(wù)未提交的變更腰懂,但是由于能夠讀到已經(jīng)提交的事務(wù),因此會出現(xiàn)幻讀和不重復(fù)讀项秉。
也就是說绣溜,RC的快照讀是讀取最新版本數(shù)據(jù),而RR的快照讀是讀取被next-key鎖作用區(qū)域的副本
4.3 RR級別下能否避免幻讀娄蔼?
我們先來模擬一下RR隔離級別下沒有出現(xiàn)幻讀的情況:
開啟第一個事務(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)
這兩個事務(wù)的執(zhí)行,有兩個問題:
1.為什么之前的例子中岁诉,在第二個事務(wù)的INSERT被阻塞了锚沸,而這次卻執(zhí)行成功了。
這是因?yàn)樵瓉淼恼Z句中帶有for update唉侄,這種讀取是當(dāng)前讀咒吐,會加鎖。而本次第一個事務(wù)中的SELECT僅僅是快照讀属划,沒有加任何鎖恬叹。所以不會阻塞其他的插入。
2.數(shù)據(jù)庫中的數(shù)據(jù)已經(jīng)改變同眯,為什么會讀不到绽昼?
這個就是之前提到的next-key lock鎖定的副本。RC及以下級別才會讀到已經(jīng)提交的事務(wù)须蜗。更多的業(yè)務(wù)邏輯是希望在某段時間內(nèi)或者某個特定的邏輯區(qū)間中硅确,前后查詢到的數(shù)據(jù)是一致的,當(dāng)前事務(wù)是和其他事務(wù)隔離的明肮。這也是數(shù)據(jù)庫在設(shè)計實(shí)現(xiàn)時遵循的ACID原則菱农。
再給出RR條件下出現(xiàn)幻讀的情形,這種情形不需要兩個事務(wù)柿估,一個事務(wù)就已經(jīng)可以說明循未,
mysql> start transaction;Query OK, 0 rows affected (0.00 sec)mysql>select * fromtestwhereid>8;+----+------+| id | code |+----+------+| 10 |? 10 |+----+------+1 row in set (0.00 sec)mysql>updatetestsetcode=9whereid=10;Query OK, 1 row affected (0.00 sec)Rows matched: 1? Changed: 1? Warnings: 0mysql>select * fromtestwhereid>8;+----+------+| id | code |+----+------+| 10 |? ? 9 |+----+------+1 row in set (0.00 sec)
至于RR隔離級別下到底會不會出現(xiàn)幻讀,就需要看幻讀的定義中的查詢到底是連續(xù)的查詢還是不連續(xù)的查詢秫舌。如果認(rèn)為RR級別下可能會出現(xiàn)幻讀的妖,那該級別下也會出現(xiàn)不重復(fù)讀。
RR隔離級別下足陨,雖然不會出現(xiàn)幻讀嫂粟,但是會因此產(chǎn)生其他的問題。
前提:當(dāng)前數(shù)據(jù)表中只存在(1,1),(5,5),(10,10)三組數(shù)據(jù)墨缘。
如果數(shù)據(jù)庫隔離級別不是默認(rèn)星虹,可以執(zhí)行SET SESSION tx_isolation='REPEATABLE-READ';(該語句不是全局設(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)
開啟第二個事務(wù)媚值,插入(11,11)并提交
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)
事務(wù)一再查詢一次,由于RR級別并沒有讀到更新
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還會有丟失更新的問題。
如下表給出的操作:
事務(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)
開啟第二個事務(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條
這個例子里京闰,事務(wù)一的更新是無效的,盡管在這個事務(wù)里程序認(rèn)為還存在(10,10)記錄甩苛。
事務(wù)一中更新之前的SELECT操作是快照讀蹂楣,所以讀到了快照里的(10,10),而UPDATE中的WHERE子句是當(dāng)前讀讯蒲,取得是最新版本的數(shù)據(jù)痊土,所以matched: 0 Changed: 0。
如果上述例子中的操作是對同一條記錄做修改墨林,就會引起更新丟失赁酝。例如,事務(wù)一和二同時開啟旭等,事務(wù)一先執(zhí)行update test set code=100 where id=10;酌呆,事務(wù)二再執(zhí)行update test set code=200 where id=10;,事務(wù)一的更新就會被覆蓋搔耕。
這就是經(jīng)典的丟失更新問題隙袁,英文叫Lost Update,又叫提交覆蓋度迂,因?yàn)槭亲詈髨?zhí)行更新的事務(wù)提交導(dǎo)致的覆蓋藤乙。還有一種更新丟失叫做回滾覆蓋猜揪,即一個事務(wù)的回滾把另一個事務(wù)提交的數(shù)據(jù)給回滾覆蓋了惭墓,但是目前市面上所有的數(shù)據(jù)庫都不支持這種stupid的操作,因此不再詳述而姐。
4.5.2 樂觀鎖與悲觀鎖
這種情況下腊凶,引入我們常見的兩種方式來解決該問題
樂觀鎖:在UPDATE的WHERE子句中加入版本號信息來確定修改是否生效
悲觀鎖:在UPDATE執(zhí)行前,SELECT后面加上FOR UPDATE來給記錄加鎖,保證記錄在UPDATE前不被修改钧萍。SELECT ... FOR UPDATE是加上了X鎖褐缠,也可以通過SELECT ... LOCK IN SHARE MODE加上S鎖,來防止其他事務(wù)對該行的修改风瘦。
無論是樂觀鎖還是悲觀鎖队魏,使用的思想都是一致的,那就是當(dāng)前讀万搔。樂觀鎖利用當(dāng)前讀判斷是否是最新版本胡桨,悲觀鎖利用當(dāng)前讀鎖定行。
但是使用樂觀鎖時仍然需要非常謹(jǐn)慎瞬雹,因?yàn)镽R是可重復(fù)讀的昧谊,一定不能在UPDATE之前先把版本號讀取出來。
5. InnoDB對不同語句執(zhí)行時的加鎖狀況
如果一個SQL語句要對二級索引(非主鍵索引)設(shè)置X模式的Record鎖酗捌,InnoDB還會檢索出相應(yīng)的聚簇索引(主鍵索引)并對它們設(shè)置鎖定呢诬。
5.1 SELECT ... FROM...不加鎖
SELECT ... FROM是快照讀取,除了SERIALIZABLE的事務(wù)隔離級別胖缤,該SQL語句執(zhí)行時不會加任何鎖尚镰。
SERIALIZABLE級別下,SELECT語句的執(zhí)行會在遇到的索引記錄上設(shè)置S模式的next-key鎖草姻。但是對于唯一索引钓猬,只鎖定索引記錄,而不會鎖定gap撩独。
5.2 UPDATE系列
S鎖讀取(SELECT ... LOCK IN SHARE MODE)敞曹,X鎖讀取(SELECT ... FOR UPDATE)、更新UPDATE和刪除DELETE這四類語句综膀,采用的鎖取決于搜索條件中使用的索引類型澳迫。
如果使用唯一索引,InnoDB僅鎖定索引記錄本身剧劝,不鎖定間隙橄登。
如果使用非唯一索引,或者未命中索引讥此,InnoDB使用間隙鎖或者next-key鎖來鎖定索引范圍拢锹,這樣就可以阻止其他事務(wù)插入鎖定范圍。
5.2.1 UPDATE語句
UPDATE ... WHERE ...在搜索遇到的每條記錄上設(shè)置一個獨(dú)占的next-key鎖萄喳,如果是唯一索引只鎖定記錄卒稳。
當(dāng)UPDATE修改聚簇索引時,將對受影響的二級索引采用隱式鎖他巨,隱式鎖是在索引中對二級索引的記錄邏輯加鎖充坑,實(shí)際上不產(chǎn)生鎖對象减江,不占用內(nèi)存空間。
例如update test set code=100 where id=10;執(zhí)行的時候code=10的索引(code是二級索引捻爷,見文中給出的建表語句)會被加隱式鎖辈灼,只有隱式鎖產(chǎn)生沖突時才會變成顯式鎖(如S鎖、X鎖)也榄。即此時另一個事務(wù)也去更新id=10這條記錄巡莹,隱式鎖就會升級為顯示鎖。
這樣做的好處是降低了鎖的開銷甜紫。
UPDATE可能會導(dǎo)致新的普通索引的插入榕莺。當(dāng)新的索引插入之前,會首先執(zhí)行一次重復(fù)索引檢查棵介。在重復(fù)檢查和插入時钉鸯,更新操作會對受影響的二級索引記錄采用共享鎖定(S鎖)。
5.2.2 DELETE語句
DELETE FROM ... WHERE ...在搜索遇到的每條記錄上設(shè)置一個獨(dú)占的next-key鎖,如果是唯一索引只鎖定記錄邮辽。
5.3 INSERT
INSERT區(qū)別于UPDATE系列單獨(dú)列出唠雕,是因?yàn)樗奶幚矸绞捷^為特別。
插入行之前吨述,會設(shè)置一種插入意向鎖岩睁,插入意向鎖表示插入的意圖。如果其它事務(wù)在要插入的位置上設(shè)置了X鎖揣云,則無法獲取插入意向鎖捕儒,插入操作也因此阻塞。
INSERT在插入的行上設(shè)置X鎖邓夕。該鎖是一個Record鎖刘莹,并不是next-key鎖,即只鎖定記錄本身焚刚,不鎖定間隙点弯,因此不會阻止其他會話在這行記錄前的間隙中插入新的記錄。
具體的加鎖過程矿咕,見6.2抢肛。
6. 可能的死鎖場景
6.1 Duplicate key error引發(fā)的死鎖
并發(fā)條件下,唯一鍵索引沖突可能會導(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í)行回滾時剥悟,事務(wù)二和事務(wù)三發(fā)生了死鎖灵寺。InnoDB的死鎖檢測一旦檢測到死鎖發(fā)生,會自動失敗其中一個事務(wù)区岗,因此看到的結(jié)果是一個失敗另一個成功略板。
為什么會死鎖?
死鎖產(chǎn)生的原因是事務(wù)一插入記錄時慈缔,對(2,2)記錄加X鎖叮称,此時事務(wù)二和事務(wù)三插入數(shù)據(jù)時檢測到了重復(fù)鍵錯誤,此時事務(wù)二和事務(wù)三要在這條索引記錄上設(shè)置S鎖藐鹤,由于X鎖的存在瓤檐,S鎖的獲取被阻塞。
事務(wù)一回滾娱节,由于S鎖和S鎖是可以兼容的挠蛉,因此事務(wù)二和事務(wù)三都獲得了這條記錄的S鎖,此時其中一個事務(wù)希望插入肄满,則該事務(wù)期望在這條記錄上加上X鎖谴古,然而另一個事務(wù)持有S鎖,S鎖和X鎖互相是不兼容的稠歉,兩個事務(wù)就開始互相等待對方的鎖釋放掰担,造成了死鎖。
事務(wù)二和事務(wù)三為什么會加S鎖怒炸,而不是直接等待X鎖
事務(wù)一的insert語句加的是隱式鎖(隱式的Record鎖带饱、X鎖),但是其他事務(wù)插入同一行記錄時阅羹,出現(xiàn)了唯一鍵沖突勺疼,事務(wù)一的隱式鎖升級為顯示鎖。
事務(wù)二和事務(wù)三在插入之前判斷到了唯一鍵沖突捏鱼,是因?yàn)椴迦肭暗?b>重復(fù)索引檢查恢口,這次檢查必須進(jìn)行一次當(dāng)前讀,于是非唯一索引就會被加上S模式的next-key鎖穷躁,唯一索引就被加上了S模式的Record鎖耕肩。
因?yàn)椴迦牒透轮岸家M(jìn)行重復(fù)索引檢查而執(zhí)行當(dāng)前讀操作,所以RR隔離級別下问潭,同一個事務(wù)內(nèi)不連續(xù)的查詢猿诸,可能也會出現(xiàn)幻讀的效果(但個人并不認(rèn)為RR級別下也會出現(xiàn)幻讀,幻讀的定義應(yīng)該是連續(xù)的讀取)狡忙。而連續(xù)的查詢由于都是讀取快照梳虽,中間沒有當(dāng)前讀的操作,所以不會出現(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ù)在插入時經(jīng)過以下幾個過程:
假設(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)匿醒,此時先要獲取插入意向鎖,由于事務(wù)一已經(jīng)在對應(yīng)的記錄和間隙上加了X鎖缠导,因此事務(wù)二被阻塞廉羔,并且阻塞的原因是獲取插入意向鎖時被事務(wù)一的X鎖阻塞。
獲取意向鎖之后僻造,插入之前進(jìn)行重復(fù)索引檢查蜜另。重復(fù)索引檢查為當(dāng)前讀,需要添加S鎖嫡意。
如果是已經(jīng)存在唯一索引举瑰,且索引未加鎖。直接拋出Duplicate key的錯誤蔬螟。如果存在唯一索引此迅,且索引加鎖,等待鎖釋放旧巾。
重復(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 |+----+------+3rowsinset (0.01sec)
事務(wù)一事務(wù)二
begin;begin;
select * from test where code=5 for update;select * from test where code=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的加鎖范圍
首先回顧一下兩個事務(wù)中的select ... for update做了哪些加鎖操作搅窿。
code=5時,首先會獲取code=5的索引記錄鎖(Record鎖)隙券,根據(jù)之前gap鎖的介紹男应,會在前一個索引和當(dāng)前索引之間的間隙加鎖,于是區(qū)間(1,5)之間被加上了X模式的gap鎖娱仔。除此之外RR模式下沐飘,還會加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ù)同時加在一個間隙上锌订。上述兩個select ... for update語句出現(xiàn)了間隙鎖的交集竹握,code=5的next-key鎖和code=10的gap鎖有重疊的區(qū)域——(5,10)。
死鎖的成因
當(dāng)事務(wù)一執(zhí)行插入語句時辆飘,會先加X模式的插入意向鎖啦辐,即兼容矩陣中的IX鎖。
但是由于插入意向鎖要鎖定的位置存在X模式的gap鎖蜈项。兼容矩陣中IX和X鎖是不兼容的芹关,因此事務(wù)一的IX鎖會等待事務(wù)二的gap鎖釋放。
事務(wù)二也執(zhí)行插入語句紧卒,與事務(wù)一同樣侥衬,事務(wù)二的插入意向鎖IX鎖會等待事務(wù)一的gap鎖釋放。
兩個事務(wù)互相等待對方先釋放鎖跑芳,因此出現(xiàn)死鎖轴总。
7 總結(jié)
除了以上給出的幾種死鎖模式,還有很多其他死鎖的場景博个。
無論是哪種場景怀樟,萬變不離其宗,都是由于某個區(qū)間上或者某一個記錄上可以同時持有鎖盆佣,例如不同事務(wù)在同一個間隙gap上的鎖不沖突往堡;不同事務(wù)中,S鎖可以阻塞X鎖的獲取共耍,但是不會阻塞另一個事務(wù)獲取該S鎖投蝉。這樣才會出現(xiàn)兩個事務(wù)同時持有鎖,并互相等待征堪,最終導(dǎo)致死鎖瘩缆。
其中需要注意的點(diǎn)是,增佃蚜、刪庸娱、改的操作都會進(jìn)行一次當(dāng)前讀操作着绊,以此獲取最新版本的數(shù)據(jù),并檢測是否有重復(fù)的索引熟尉。
這個過程除了會導(dǎo)致RR隔離級別下出現(xiàn)死鎖之外還會導(dǎo)致其他兩個問題:
第一個是可重復(fù)讀可能會因?yàn)檫@次的當(dāng)前讀操作而中斷归露,(同樣,幻讀可能也會因此產(chǎn)生)斤儿;
第二個是其他事務(wù)的更新可能會丟失(解決方式:悲觀鎖剧包、樂觀鎖)。