InnoDB如何解決幻讀狸剃?
一致性讀
一致性非鎖定讀(Consistent Nonlocking Reads)
一致性不鎖定讀(Consistent Nonlocking Reads)是InnoDB使用多版本控制(MVCC)來讀取某個時間點創(chuàng)建的快照的止。這個請求只能看到這個時間點之前提交的事物的修改声诸,
看不到之后的或者未提交的事務的修改赠幕。如下圖:
快照讀:讀取的是快照版本殖妇,也就是歷史版本买猖。普通的SELECT就是快照讀
當前讀:讀取的是最新版本改橘。UPDATE、DELETE玉控、INSERT飞主、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是當前讀高诺。
什么是幻讀碌识?
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if aSELECTis executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
事務A 按照一定條件進行數(shù)據(jù)讀取, 期間事務B 插入了相同搜索條件的新數(shù)據(jù)虱而,事務A再次按照原先條件進行讀取時筏餐,發(fā)現(xiàn)了事務B 新插入的數(shù)據(jù) 稱為幻讀
the exception to this rule is that the query sees the changes made by earlier statements within the same transaction. This exception causes the following anomaly: If you update some rows in a table, aSELECTsees the latest version of the updated rows, but it might also see older versions of any rows. If other sessions simultaneously update the same table, the anomaly means that you might see the table in a state that never existed in the database.
按照規(guī)則,在同一個事務中修改的內(nèi)容牡拇,在之后的查詢中應該可以看到魁瞪。但是如果其他事務對同樣的內(nèi)容做了修改,那之后的查詢就會看到本不應該看到的數(shù)據(jù)惠呼。
創(chuàng)建表导俘,并插入數(shù)據(jù):
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`)) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
?sessionAsessionB
T1begin;?
T2select * from t where d=5;? ####result: (5,5,5)?
T3?insert into t value(1,1,5)
T4update t set d=100 where d=5;?
T5select * from t where d=5;? ? ? ### result: (1,1,100) (5,5,100)?
因為sessionA中的每一條查詢語句都加了for update 排他鎖,所以都是當前讀剔蹋。讀到的都是數(shù)據(jù)庫中最新的數(shù)據(jù)旅薄。
T2時刻查詢d=5,查出來一條記錄(5,5,5)
T3時刻sessionC 插入了一條記錄(1,1,5)
T4時刻泣崩,sessionA更新了d=5的行
T5時刻少梁,再次查詢d=5洛口,得到兩個結(jié)果 (1,100,5) (5,100,5)
sessionA在一次事務中得到了兩個不同的結(jié)果,出現(xiàn)了幻讀
間隙鎖
間隙鎖顧名思義凯沪,鎖的是兩個索引記錄的間隙绍弟,或者是第一個索引記錄之前或者最后一個索引之后的間隙。
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
無論c1=15這個值是否在表中存在著洼,其他的事務都不可以對t.c1=15這個值進行插入操作樟遣。因為10到20這個間隙(gap)被加鎖了。
間隙鎖之間并不沖突身笤,一個事務在一個間隙加了間隙排他鎖豹悬,其他事務同樣可以在該間隙加排他鎖或者共享鎖
如果是唯一索引的等值查詢,間隙鎖會退化成行鎖
Next-Key Locks
next-key lock是行鎖和間隙鎖的結(jié)合液荸。
InnoDBperforms row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. A next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on recordRin an index, another session cannot insert a new index record in the gap immediately beforeRin the index order.
加鎖規(guī)則:
next-key lock 是前開后閉區(qū)間瞻佛。
查找過程中訪問到的對象才會加鎖。
索引上的等值查詢娇钱,給唯一索引加鎖的時候伤柄,next-key lock 退化為行鎖。
索引上的等值查詢文搂,向右遍歷時且最后一個值不滿足等值條件的時候适刀,next-key lock 退化為間隙鎖。
唯一索引上的范圍查詢會訪問到不滿足條件的第一個值為止煤蹭。
CREATE TABLE `t` (
? `id` int(11) NOT NULL,
? `c` int(11) DEFAULT NULL,
? `d` int(11) DEFAULT NULL,
? PRIMARY KEY (`id`),
? KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
場景一:等值查詢的間隙鎖
由于表 t 中沒有 id=7 的記錄
加鎖單位是 next-key lock笔喉,session A 加鎖范圍就是 (5,10];
這是一個等值查詢 (id=7)硝皂,而 id=10 不滿足查詢條件常挚,next-key lock 退化成間隙鎖,因此最終加鎖的范圍是 (5,10)稽物。所以奄毡,session B 要往這個間隙里面插入 id=8 的記錄會被鎖住,但是 session C 修改 id=10 這行是可以的贝或。
場景二:非唯一索引等值鎖
這里 session A 要給索引 c 上 c=5 的這一行加上讀鎖吼过。
加鎖單位是 next-key lock,因此會給 (0,5]加上 next-key lock傀缩。
c 是普通索引那先,因此僅訪問 c=5 這一條記錄是不能馬上停下來的,需要向右遍歷赡艰,查到 c=10 才放棄售淡。訪問到的都要加鎖,因此要給 (5,10]加 next-key lock。
同時這個符合: 等值判斷揖闸,向右遍歷揍堕,最后一個值不滿足 c=5 這個等值條件,因此退化成間隙鎖 (5,10)汤纸。
只有訪問到的對象才會加鎖衩茸,這個查詢使用覆蓋索引,并不需要訪問主鍵索引贮泞,所以主鍵索引上沒有加任何鎖楞慈,這就是為什么 session B 的 update 語句可以執(zhí)行完成.。
lock in share mode 只鎖覆蓋索引啃擦,但是如果是 for update 就不一樣了囊蓝。 執(zhí)行 for update 時,系統(tǒng)會認為你接下來要更新數(shù)據(jù)令蛉,因此會順便給主鍵索引上滿足條件的行加上行鎖聚霜。
場景三:主鍵索引范圍鎖
開始執(zhí)行的時候,要找到第一個 id=10 的行珠叔,因此本該是 next-key lock(5,10]蝎宇。? 主鍵 id 上的等值條件,退化成行鎖祷安,只加了 id=10 這一行的行鎖姥芥。
范圍查找就往后繼續(xù)找,找到 id=15 這一行停下來辆憔,因此需要加 next-key lock(10,15]撇眯。
場景四:非唯一索引范圍鎖
在第一次用 c=10 定位記錄的時候报嵌,索引 c 上加了 (5,10]這個 next-key lock 虱咧。
由于索引 c 是非唯一索引,也就是說不會蛻變?yōu)樾墟i锚国,因此最終 sesion A 加的鎖是腕巡,索引 c 上的 (5,10] 和 (10,15] 這兩個 next-key lock。
場景五:唯一索引范圍鎖 bug
session A 是一個范圍查詢血筑,按照原則 1 的話绘沉,應該是索引 id 上只加 (10,15]這個 next-key lock,并且因為 id 是唯一鍵豺总,所以循環(huán)判斷到 id=15 這一行就應該停止了车伞。
但是實現(xiàn)上,InnoDB 會往前掃描到第一個不滿足條件的行為止喻喳,也就是 id=20另玖。而且由于這是個范圍掃描,因此索引 id 上的 (15,20]這個 next-key lock 也會被鎖上。所以你看到了谦去,session B 要更新 id=20 這一行慷丽,是會被鎖住的。同樣地鳄哭,session C 要插入 id=16 的一行要糊,也會被鎖住。照理說妆丘,這里鎖住 id=20 這一行的行為锄俄,其實是沒有必要的。因為掃描到 id=15勺拣,就可以確定不用往后再找了珊膜。但實現(xiàn)上還是這么做了。
場景六:非唯一索引上存在"等值"的例子
給表插入一條新的記錄
mysql> insert into t values(30,10,30);
表里現(xiàn)在有兩條c=10的記錄宣脉。但是因為這兩行的主鍵id不同车柠,因此,這兩個c=10的記錄之間塑猖,也是有間隙的
session A 在遍歷的時候竹祷,先訪問第一個 c=10 的記錄。同樣地羊苟,這里加的是 (c=5,id=5) 到 (c=10,id=10) 這個 next-key lock塑陵。
session A 向右查找,直到碰到 (c=15,id=15) 這一行蜡励,循環(huán)才結(jié)束令花。這是一個等值查詢,向右查找到了不滿足條件的行凉倚,所以會退化成 (c=10,id=10) 到 (c=15,id=15) 的間隙鎖兼都。
場景七:limit 語句加鎖
session A 的 delete 語句加了 limit 2。你知道表 t 里 c=10 的記錄其實只有兩條稽寒,因此加不加 limit 2扮碧,刪除的效果都是一樣的,但是加鎖的效果卻不同杏糙∩魍酰可以看到,session B 的 insert 語句執(zhí)行通過了宏侍,跟場景六的結(jié)果不同赖淤。
這是因為,這里里的 delete 語句明確加了 limit 2 的限制谅河,因此在遍歷到 (c=10, id=30) 這一行之后咱旱,滿足條件的語句已經(jīng)有兩條嗜愈,循環(huán)就結(jié)束了。
索引 c 上的加鎖范圍就變成了從(c=5,id=5) 到(c=10,id=30) 這個前開后閉區(qū)間
在刪除數(shù)據(jù)的時候盡量加 limit莽龟。這樣不僅可以控制刪除數(shù)據(jù)的條數(shù)蠕嫁,讓操作更安全,還可以減小加鎖的范圍毯盈。
場景八:一個死鎖的例子
session A 啟動事務后執(zhí)行查詢語句加 lock in share mode剃毒,在索引 c 上加了 next-key lock(5,10] 和間隙鎖 (10,15);
session B 的 update 語句也要在索引 c 上加 next-key lock(5,10] 搂赋,進入鎖等待赘阀;
然后 session A 要再插入 (8,8,8) 這一行,被 session B 的間隙鎖鎖住脑奠。由于出現(xiàn)了死鎖基公,InnoDB 讓 session B 回滾。
session B 的“加 next-key lock(5,10] ”操作宋欺,實際上分成了兩步轰豆,先是加 (5,10) 的間隙鎖,加鎖成功齿诞;然后加 c=10 的行鎖酸休,這時候才被鎖住的。
在RR的隔離級別下祷杈,Innodb使用MVCC和next-key locks解決幻讀斑司,MVCC解決的是普通讀(快照讀)的幻讀,next-key locks解決的是當前讀情況下的幻讀但汞。