1. 程式人生 > >20 幻讀是什麼,幻讀有什麼問題?

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次查詢,分別是Q1Q2Q3,他們的sql語句相同,都是select * from t20 where d=5 for update。這個語句應該很清楚了,

查所有d=5的行,而且使用的是當前讀,並且加上寫鎖。現在,我們來看一下這三sql語句,分別回返回什麼結果。

  1. Q1只返回id=5這一行;
  2. T2時刻,session Bid=0這一行的d值改成了5,因此T3時刻Q2查出來的是id=0id=5的這兩行。
  3. T4時刻,session C又插入了一行(1,1,5),因此T5時刻Q3查詢出的是id=0id=1id=5這三行。

其中,Q3讀到id=1這一行的現象,被稱為”幻讀”,也就是說,幻讀是指在同一個事務中,前後兩個相同的查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行

這裡,需要對”幻讀”做一個說明

  1. rr隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的資料的,因此,幻讀在當前讀才會出現
  2. 上面session B的修改結果,被session A之後的select 語句用”當前讀”看到,不能稱為幻讀,幻讀僅專指新插入的行

根據事務的可見性規則來分析,上面的三個查詢都加了for update,都是當前讀。而根據當前讀的規則,就是要能讀到所有已經提交的記錄的最新值。

並且session Bsession C的兩條語句,執行後就會提交,所以Q2Q3看到就應該是兩個事務的操作結果。

但是,這是不是真的沒有問題呢?

不,這裡是有問題的

幻讀有什麼問題?

首先語義上的,SESSION AT1時刻就聲明瞭,我要把所有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=0d=5的這一會的c值,改成了5

由於在T1時刻,session A還只是給id=5這一行加了行鎖,並沒有給id=0這行加上鎖,因此session BT2時刻,是可以執行這兩條update的語句,這樣,就破壞了session AQ1語句要鎖住所有d=5的行的加鎖宣告。

其次,是資料一致性的問題

我們知道,鎖的設計是為了保證資料的一致性,而這個一致性,不止是資料庫內部資料狀態在此刻的一致性,還包含了資料和日誌在邏輯上的一致性。

為了說明這個問題,在session AT1時刻加上一個更新語句:

 

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=0id=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,要把整個表所有記錄鎖起來,

就形成了7next-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 Ainsert語句報錯返回了。

現在知道,間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,這其實是影響併發的,其實,這還只是一個簡單的例子。下一篇會有更多的例子

小結

提到全表掃描的加鎖方式,我們發現即使給所有行加上行鎖,仍然無法解決幻讀的問題,因此引入了間隙鎖的概念。

思考題

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 Bsession Cinsert語句會進入到什麼狀態