20 幻讀是什麼,幻讀有什麼問題?
例子:
CREATE TABLE `t20` (
`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 t20 values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
select * from t20;
幻讀是什麼?
|
SESSION A |
SESSION B |
SESSION C |
T1 |
begin; select * from t20 where d=5 for update;/*Q1*/ result:(5,5,5) |
|
|
T2 |
|
update t20 set d=5 where id=0; |
|
T3 |
select * from t20 where d=5 for update;/*Q2*/ result:(0,0,5),(5,5,5) |
|
|
T4 |
|
|
insert into t20 values(1,1,5); |
T5 |
select * from t20 where d=5 for update;/*Q3*/ result:(0,0,5),(1,1,5),(5,5,5) |
|
|
T6 |
commit; |
|
|
假設只在id=5這一行加行鎖
可以看到,session A裡執行了3次查詢,分別是Q1,Q2,Q3,他們的sql語句相同,都是select * from t20 where d=5 for update。這個語句應該很清楚了,
查所有d=5的行,而且使用的是當前讀,並且加上寫鎖。現在,我們來看一下這三sql語句,分別回返回什麼結果。
- Q1只返回id=5這一行;
- 在T2時刻,session B把id=0這一行的d值改成了5,因此T3時刻Q2查出來的是id=0和id=5的這兩行。
- 在T4時刻,session C又插入了一行(1,1,5),因此T5時刻Q3查詢出的是id=0,id=1,id=5這三行。
其中,Q3讀到id=1這一行的現象,被稱為”幻讀”,也就是說,幻讀是指在同一個事務中,前後兩個相同的查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行。
這裡,需要對”幻讀”做一個說明:
- 在rr隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的資料的,因此,幻讀在當前讀才會出現。
- 上面session B的修改結果,被session A之後的select 語句用”當前讀”看到,不能稱為幻讀,幻讀僅專指”新插入的行”。
根據事務的可見性規則來分析,上面的三個查詢都加了for update,都是當前讀。而根據當前讀的規則,就是要能讀到所有已經提交的記錄的最新值。
並且session B和session C的兩條語句,執行後就會提交,所以Q2和Q3看到就應該是兩個事務的操作結果。
但是,這是不是真的沒有問題呢?
不,這裡是有問題的
幻讀有什麼問題?
首先語義上的,SESSION A在T1時刻就聲明瞭,我要把所有d=5的行鎖住,不準別的進行讀寫操作,而實際上,這個語義被破壞了。
|
SESSION A |
SESSION B |
SESSION C |
T1 |
begin; select * from t20 where d=5 for update;/*Q1*/ |
|
|
T2 |
|
update t20 set d=5 where id=0; update t20 set c=5 where id=0; |
|
T3 |
select * from t20 where d=5 for update;/*Q2*/ |
|
|
T4 |
|
|
insert into t20 values(1,1,5); update t20 set c=5 where id=1; |
T5 |
select * from t20 where d=5 for update;/*Q3*/ |
|
|
T6 |
commit; |
|
|
SESSION B的第二條語句update t20 set c=5 where id=0的語義是”我把id=0,d=5的這一會的c值,改成了5”
由於在T1時刻,session A還只是給id=5這一行加了行鎖,並沒有給id=0這行加上鎖,因此session B在T2時刻,是可以執行這兩條update的語句,這樣,就破壞了session A的Q1語句要鎖住所有d=5的行的加鎖宣告。
其次,是資料一致性的問題。
我們知道,鎖的設計是為了保證資料的一致性,而這個一致性,不止是資料庫內部資料狀態在此刻的一致性,還包含了資料和日誌在邏輯上的一致性。
為了說明這個問題,在session A的T1時刻加上一個更新語句:
|
SESSION A |
SESSION B |
SESSION C |
T1 |
begin; select * from t20 where d=5 for update;/*Q1*/ update t20 set d=100 where d=5; |
|
|
T2 |
|
update t20 set d=5 where id=0; update t20 set c=5 where id=0; |
|
T3 |
select * from t20 where d=5 for update;/*Q2*/ |
|
|
T4 |
|
|
insert into t20 values(1,1,5); update t20 set c=5 where id=1; |
T5 |
select * from t20 where d=5 for update;/*Q3*/ |
|
|
T6 |
commit; |
|
|
-update的加鎖語義和select ... for update是一致的,所以這時候加上這條update語句很合理。Session A宣告說”要給d=5的語句加鎖鎖”,就是為了要更新資料,新加的這條update就是把它認為加上了鎖的這一行的d值修改成100.
現在,我們來分析一下圖3執行完成後,資料庫是什麼結果
1 經過T1時刻,id=5這一行變成(5,5,100),當然這個結果最終在T6時刻正是提交的
2 經過T2時刻,id=0這一行變成(0,5,5)
3 經過T4時刻,表裡面多了一行(1,5,5)
4 其他行跟這個執行序列無關,保持不變。
這時候看資料也沒啥問題,我們看看這個時候的binlog內容。
1 T2時刻,session B事務提交了,寫入了兩條語句
2 T4時刻,session C事務提交了,寫入了兩條語句
3 T6時刻,session A事務提交,寫入了update t set d=100 where d=5這條語句。放一起的話。
update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/
insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/
update t set d=100 where d=5;/* 所有 d=5 的行,d 改成 100*/
這個時候應該出問題了,這個語句序列,不論是拿到備庫去執行,還是以後用binlog來恢復,這3行的結果,都變成了(0,5,100),(1,5,100)和(5,5,100)。
也就是id=0和id=1這兩行,發生了資料不一致,這個問題很嚴重,是不行的。
到這裡,在想一想,這個資料不一致是怎麼引入的。
分析知道,這是我們假設”select * from t20 where d=5 for update這條語句只給d=5這一行,也就是id=5的這一行加鎖”導致的。
所以我們認為,上面的設計是不合理的。
那怎麼改呢?我們把掃描過程中碰到的行,都加上寫鎖,再來看看執行效果
|
SESSION A |
SESSION B |
SESSION C |
T1 |
begin; select * from t20 where d=5 for update;/*Q1*/ update t20 set d=100 where d=5; |
|
|
T2 |
|
update t20 set d=5 where id=0; (blocked) update t20 set c=5 where id=0; |
|
T3 |
select * from t20 where d=5 for update;/*Q2*/ |
|
|
T4 |
|
|
insert into t20 values(1,1,5); update t20 set c=5 where id=1; |
T5 |
select * from t20 where d=5 for update;/*Q3*/ |
|
|
T6 |
commit; |
|
|
由於session A把所有的行都加了寫鎖,所以session B在執行第一個udpate語句的時候就被鎖住了,需要等到T6時刻session A提交以後,session B才能繼續執行。
在binlog裡面,執行序列是這樣的
insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/
update t set d=100 where d=5;/* 所有 d=5 的行,d 改成 100*/
update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/
可以看到,按照日誌順序執行,id=0這一行的最終結果是(0,5,5),所以,id=0這一行的問題解決了。
但同時你也可以看到,id=1這一行,在資料庫裡面的結果是(1,5,5),而根據binlog的執行結果是(1,5,100),也就說幻讀的問題還沒有解決。
原因很簡單,在T3時刻,我們給所有行加鎖的時候,id=1這一行還不存在,不存在也就加不上鎖。
也就說,即使把所有的記錄都加上鎖,還是阻止不了新插入的記錄,這也是為什麼”幻讀”會被單獨拿出來解決的問題。
到這裡,其實我們剛說明完文章的標題,幻讀的定義和幻讀有什麼問題
在innodb,我們看是怎麼解決幻讀問題的
如何解決幻讀?
現在知道,產生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的”間隙”,因此,為了解決幻讀問題,innodb引入了新的鎖,也即是間隙鎖(gap lock)。
顧名思義,間隙鎖,鎖的就是兩個值之間的空隙。
這樣,當執行select * from t20 where d=5 for update的時候,就不止給資料庫已有的6行記錄加上了行鎖,還同時加了7個間隙鎖,這樣就確保無法在插入新的記錄。
也就是說這時候,在一行掃描的過程中,不僅將給行上加了行鎖,還給行兩邊的空隙,也加上了間隙鎖。
現在你知道了,資料行是可以加上鎖的實體,資料行之間的間隙,也是可以加上鎖的實體。但是間隙鎖跟我們碰到過的鎖都不太一樣。
比如行鎖,分讀鎖和寫鎖
|
讀鎖 |
寫鎖 |
讀鎖 |
相容 |
衝突 |
寫鎖 |
衝突 |
衝突 |
也就說,跟行鎖衝突關係的是”另外一個行鎖”
但是間隙鎖不一樣,跟間隙鎖存在衝突關係的,是”往這個間隙中插入一個記錄”這個操作。間隙鎖之間是不存在衝突關係的。
SESSION A |
SESSION B |
begin; select * from t20 where c=7 lock in share mode; |
|
|
begin; select * from t20 where c=7 for update; |
這裡session B並不會被鎖住,因為表t20裡並沒有c=7這個記錄,因此session A加的是間隙鎖(5,10),而session B也是在這個間隙加的間隙鎖,它們的共同目標,即:保護這個間隙,不允許插入值,但,它們之間是不衝突的。
間隙鎖和行鎖合稱為next- key lock,每個next-key lock是前開後閉區間。也就是說,我們的表t20初始化後,如果用select * from t20 for update,要把整個表所有記錄鎖起來,
就形成了7個next-key lock,分別是(-∞,0],(0,5],(5,10],(10,15],(15,20],(20,25],(25,+supernum].
間隙鎖和next-key lock的引入,幫我們解決了幻讀的問題,但同時也帶來了一些困擾。
SESSION A |
SESSION B |
begin; select * from t20 where id=9 for update; |
|
|
begin; select * from t20 where id=9 for update; |
|
insert into t20 values (9,9,9); (blocked) |
insert into t20 values(9,9,9); ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
LATEST DETECTED DEADLOCK ------------------------ 2019-01-04 17:03:20 7f662466a700 *** (1) TRANSACTION: TRANSACTION 5580608, ACTIVE 17 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 376, 2 row lock(s) MySQL thread id 18034, OS thread handle 0x7f6623fd0700, query id 637155 127.0.0.1 system update insert into t20 values (9,9,9) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 1276 page no 3 n bits 80 index `PRIMARY` of table `test`.`t20` trx id 5580608 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000053c93a; asc S :;; 2: len 7; hex 6e000002620570; asc n b p;; 3: len 4; hex 8000000a; asc ;; 4: len 4; hex 8000000a; asc ;; *** (2) TRANSACTION: TRANSACTION 5580596, ACTIVE 20 sec inserting mysql tables in use 1, locked 1 3 lock struct(s), heap size 376, 2 row lock(s) MySQL thread id 18046, OS thread handle 0x7f662466a700, query id 637171 127.0.0.1 system update insert into t20 values (9,9,9) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 1276 page no 3 n bits 80 index `PRIMARY` of table `test`.`t20` trx id 5580596 lock_mode X locks gap before rec Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000053c93a; asc S :;; 2: len 7; hex 6e000002620570; asc n b p;; 3: len 4; hex 8000000a; asc ;; 4: len 4; hex 8000000a; asc ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 1276 page no 3 n bits 80 index `PRIMARY` of table `test`.`t20` trx id 5580596 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 8000000a; asc ;; 1: len 6; hex 00000053c93a; asc S :;; 2: len 7; hex 6e000002620570; asc n b p;; 3: len 4; hex 8000000a; asc ;; 4: len 4; hex 8000000a; asc ;; *** WE ROLL BACK TRANSACTION (2) ------------
死鎖分析:
1 session A執行select ... for update,由於id=9這一行並不存在,因此會加上間隙鎖(5,10)
2 session B 執行select... for update,同樣會加上間隙鎖(5,10),間隙鎖之間並不衝突,因此這個會執行成功。
3 session B試圖插入一行(9,9,9),被session A的間隙鎖擋住了,只好進入等待
4 session A試圖插入一行(9,9,9),被session B的間隙擋住了。
至此,兩個session進入互相等待狀態,形成了死鎖,當然,innodb的死鎖檢測馬上發現這對死鎖關係,讓session A的insert語句報錯返回了。
現在知道,間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,這其實是影響併發的,其實,這還只是一個簡單的例子。下一篇會有更多的例子
小結
提到全表掃描的加鎖方式,我們發現即使給所有行加上行鎖,仍然無法解決幻讀的問題,因此引入了間隙鎖的概念。
思考題
SESSION A |
SESSION B |
SESSION C |
begin; select * from t20 where c>=15 and c<=20 order by c desc for update; |
|
|
|
insert into t20 values(11,11,11); |
|
|
|
insert into t20 values(6,6,6); |
這裡的session B和session C的insert語句會進入到什麼狀態