1. 程式人生 > 實用技巧 >MVCC能否解決幻讀

MVCC能否解決幻讀

參考:

https://blog.csdn.net/qq_35590091/article/details/107734005

https://blog.csdn.net/ashic/article/details/53735537

MVCC能否解決幻讀?

“幻讀”指,同一個事務裡面連續執行兩次同樣的sql語句,可能導致不同結果的問題,第二次sql語句可能會返回之前不存在的行。

先給出結論:不能籠統的說能不能解決,因為有的情況下可以解決,但是有的情況下解決不了。


可以解決的情況

mysql裡面實際上有兩種讀,一種是“快照讀”,比如我們使用select進行查詢,就是快照讀,在“快照讀"的情況下是可以解決“幻讀”的問題的。使用的就是MVCC,具體來說如下圖,幾個事務併發執行:

可以看到,儘管別的事務已經提交插入和更新,但是事務A的select讀取的還是一樣的。具體就是mvcc利用歷史版本資訊(快照)來控制他能讀取的資料的範圍。具體的可以看看我的關於MVCC淺析的文章。


另外一種讀是:“當前讀”。對於會對資料修改的操作(update、insert、delete)都是採用當前讀的模式,此外,下面兩個語句也是當前讀:

1、select * from table where ? lock in share mode; (加共享鎖)

2、select * from table where ? for update; (加排它鎖)

因此總結一下,下面幾個語句都是當前讀,都會讀取最新的快照資料,都會加鎖(除了第一個加共享鎖,其他都是互斥鎖):

select * from table where ? lock in share mode; 
select * from table where ? for update; 
insert; 
update; 
delete;

 

在執行這幾個操作時會讀取最新的記錄,即使是別的事務提交的資料也可以查詢到。比如要update一條記錄,但是在另一個事務中已經delete掉這條資料並且commit了,如果update就會產生衝突,所以在update的時候需要知道最新的資料。讀取的是最新的資料,並且需要加鎖(排它鎖或者共享鎖)。

舉個例子,下面是在可重複讀級別下,事務1在update後,對該資料加鎖,事務B無法插入新的資料,這樣事務A在update前後資料保持一致,避免了幻讀,可以明確的是,update鎖的肯定不只是已查詢到的幾條資料,因為這樣無法阻止insert,有同學會說,那就是鎖住了整張表唄,其實不是,其實這裡的鎖,是next-keylocking(就是一個行鎖+範圍鎖)實現的.行鎖不必說,就是更新的時候鎖住這一行,這樣別的事務就不能同時進行修改操作了。範圍鎖(gap lock)鎖則是防止插入。


什麼是next key lock?

所謂的next key lock就是一個行鎖(record lock)+範圍鎖(gap lock),比如某一個輔助索引(比如上面的class_id),如果它有1,3,5這幾個值,那麼當我們使用next key lock的鎖住class_id=1的時候,實際上鎖住了(-無窮,1],或者鎖住class_id=3的時候,實際上鎖住的是(1,3],也就是一個左開右閉的區間。如果此時別的事務要在這個區間內插入資料,就會被阻塞住。這個鎖一直到事務提交才會釋放。因此,即使出現了上面圖片裡面這種情況,也可以保證前後兩次去讀的內容一致,因為對這個輔助索引上的鎖是:“next key lock”,他會鎖住一個區間。

但是注意,對於可重複讀預設使用的就是next key lock,但是對於“唯一索引”,比如主鍵的索引,next key lock會降級成行鎖,而不會鎖住一個區間。因此,如果上面的事務1的update使用的是主鍵,事務2也使用主鍵進行插入,那麼實際上事務2根本不會被阻塞,可以立即插入並返回。而對於非唯一索引,next key lock則不會降級。


什麼情況MVCC也會出現幻讀?

下面這樣的情況:

1.a事務先select,b事務insert確實會加一個gap鎖,但是如果b事務commit,這個gap鎖就會釋放(釋放後a事務可以隨意操作),

2.a事務再select出來的結果在MVCC下還和第一次select一樣,

3.接著a事務不加條件地update,這個update會作用在所有行上(包括b事務新加的),

4.a事務再次select就會出現b事務中的新行,並且這個新行已經被update修改了.

上面這樣,事務2提交之後,事務1再次執行update,因為這個是當前讀,他會讀取最新的資料,包括別的事務已經提交的,所以就會導致此時前後讀取的資料不一致,出現幻讀。

參考:

https://www.cnblogs.com/CoderAyu/p/11525408.html

Mysql(Innodb)如何避免幻讀

幻讀Phantom Rows

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.

幻讀問題是指一個事務的兩次不同時間的相同查詢返回了不同的的結果集。例如:一個 select 語句執行了兩次,但是在第二次返回了第一次沒有返回的行,那麼這些行就是“phantom” row.

read view(或者說 MVCC)實現了一致性不鎖定讀(Consistent Nonlocking Reads),從而避免了(非當前讀下)幻讀

實驗1:

開兩個視窗設定

set session tx_isolation='REPEATABLE-READ';
select @@session.autocommit;select @@global.tx_isolation,@@session.tx_isolation;

create table read_view(text varchar(50));
insert into read_view values('init');

兩個會話開始事務

SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)

SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)

SESSION_A執行一個查詢,這個查詢可以訪問任何表,這個查詢的目的是建立一個當前時間點的快照
START TRANSACTION WITH CONSISTENT SNAPSHOT;也可以達到同樣的效果

SESSION_A>select * from dept;
+--------+------------+----------+
| deptno | dname      | loc      |
+--------+------------+----------+
|     10 | ACCOUNTING | NEW YORK |
|     20 | RESEARCH   | DALLAS   |
|     30 | SALES      | CHICAGO  |
|     40 | OPERATIONS | BOSTON   |
+--------+------------+----------+
4 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

SESSION_B 插入一條記錄並提交

SESSION_B>insert into read_view values('after session A select');
Query OK, 1 row affected (0.01 sec)

SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5

SESSION_A

SESSION_A>select * from read_view;
+------+
| text |
+------+
| init |
+------+
1 row in set (0.00 sec)

SESSION_A>commit;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from read_view;
+------------------------+
| text                   |
+------------------------+
| init                   |
| after session A select |
+------------------------+
2 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

由於 SESSION_A 第一次的查詢開始於 SESSION_B 插入資料前,所以建立了一個以SELECT操作的時間為基準點的 read view,避免了幻讀的產生
所以在 SESSION_A 的事務結束前,無法看到 SESSION_B 對錶 read_view 做出的任何更改 (insert,delete,update)

實驗2

兩個會話開始事務

SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)

SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5

SESSION_B 在 SESSION_A 建立read view 前插入資料

SESSION_B>insert into read_view values('before Session_A select');
Query OK, 1 row affected (0.00 sec)

SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5

SESSION_A

SESSION_A>select * from read_view;
+-------------------------+
| text                    |
+-------------------------+
| init                    |
| after session A select  |
| before Session_A select |
+-------------------------+
3 rows in set (0.00 sec)

SESSION_A>commit
    -> ;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from read_view;
+-------------------------+
| text                    |
+-------------------------+
| init                    |
| after session A select  |
| before Session_A select |
+-------------------------+
3 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

由於 SESSION_A 第一次查詢開始於 SESSION_B 對錶做出更改並提交後,所以這次的 read view 包含了 SESSION_B 所做出的更改

在官方文件中寫道
http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html

A consistent read means that InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point of time, and no changes made by later or uncommitted transactions. 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, a SELECT sees 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.

一致性讀是通過 MVCC 為查詢提供了一個基於時間的點的快照。這個查詢只能看到在自己之前提交的資料,而在查詢開始之後提交的資料是不可以看到的。一個特例是,這個查詢可以看到於自己開始之後的同一個事務產生的變化。這個特例會產生一些反常的現象

If the transaction isolation level is REPEATABLE READ (the default level), all consistent reads within the same transaction read the snapshot established by the first such read in that transaction. You can get a fresher snapshot for your queries by committing the current transaction and after that issuing new queries.

在預設隔離級別REPEATABLE READ下,同一事務的所有一致性讀只會讀取第一次查詢時建立的快照

實驗3

兩個會話開始事務

SESSION_A開始事務並建立快照
SESSION_A>START TRANSACTION WITH CONSISTENT SNAPSHOT;
Query OK, 0 rows affected (0.00 sec)

SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)


SESSION_A>select * from read_view;
+-------------------------+
| text                    |
+-------------------------+
| init                    |
| after session A select  |
| before Session_A select |
+-------------------------+
3 rows in set (0.00 sec)


SESSION_B>insert into read_view values('anomaly'),('anomaly');
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

SESSION_B>update read_view set text='INIT' where text='init';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from read_view;
+-------------------------+
| text                    |
+-------------------------+
| init                    |
| after session A select  |
| before Session_A select |
+-------------------------+
3 rows in set (0.00 sec)

SESSION_A更新了它並沒有"看"到的行
SESSION_A>update read_view set text='anomaly!' where text='anomaly';
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2  Changed: 2  Warnings: 0

SESSION_A>select * from read_view;
+-------------------------+
| text                    |
+-------------------------+
| init                    |
| after session A select  |
| before Session_A select |
| anomaly!                |
| anomaly!                |
+-------------------------+
5 rows in set (0.00 sec)

SESSION_A>commit;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from read_view;
+-------------------------+
| text                    |
+-------------------------+
| INIT                    |
| after session A select  |
| before Session_A select |
| anomaly!                |
| anomaly!                |
+-------------------------+
5 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

觀察實驗步驟可以發現,在倒數第二次查詢中,出現了一個並不存在的狀態

the anomaly means that you might see the table in a state that never existed in the database

這裡A的前後兩次讀,均為快照讀,而且是在同一個事務中。但是B先插入直接提交,此時A再update,update屬於當前讀,所以可以作用於新插入的行,並且將修改行的當前版本號設為A的事務號,所以第二次的快照讀,是可以讀取到的,因為同事務號。這種情況符合MVCC的規則,如果要稱為一種幻讀也非不可,算為一個特殊情況來看待吧。


With READ COMMITTED isolation level, each consistent read within a transaction sets and reads its own fresh snapshot.

在 read commit 隔離級別下,同一事務的每個一致性讀sets and reads its own fresh snapshot.

實驗4

修改事務隔離級別
set session tx_isolation='READ-COMMITTED'
兩個會話開始事務

SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)

SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)


SESSION_A>select * from read_view;
+-------------------------+
| text                    |
+-------------------------+
| INIT                    |
| after session A select  |
| before Session_A select |
| anomaly!                |
| anomaly!                |
+-------------------------+
5 rows in set (0.00 sec)

SESSION_B>insert into read_view values('hehe');
Query OK, 1 row affected (0.00 sec)

SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from read_view;
+-------------------------+
| text                    |
+-------------------------+
| INIT                    |
| after session A select  |
| before Session_A select |
| anomaly!                |
| anomaly!                |
| hehe                    |
+-------------------------+
6 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

read commit 每次讀取都是新的快照

InnoDB通過Nextkey lock解決了當前讀時的幻讀問題

Innodb行鎖分為:

型別說明
Record Lock: 在索引上對單行記錄加鎖.
Gap Lock: 鎖定一個範圍的記錄,但不包括記錄本身.鎖加在未使用的空閒空間上,可能是兩個索引記錄之間,也可能是第一個索引記錄之前或最後一個索引之後的空間.
Next-Key Lock: 行鎖與間隙鎖組合起來用就叫做Next-Key Lock。鎖定一個範圍,並且鎖定記錄本身。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題。

實驗5

建立表

(mysql@localhost) [fandb]> create table t5(id int,key(id));
Query OK, 0 rows affected (0.02 sec)

SESSION_A>insert into t5 values(1),(4),(7),(10);
Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

開始實驗

SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from t5;
+------+
| id   |
+------+
|    1 |
|    4 |
|    7 |
|   10 |
+------+
4 rows in set (0.00 sec)

SESSION_A>select * from t5 where id=7 for update;
+------+
| id   |
+------+
|    7 |
+------+
1 row in set (0.00 sec)


SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)

SESSION_B>insert into t5 values(2);
Query OK, 1 row affected (0.00 sec)

SESSION_B>insert into t5 values(12);
Query OK, 1 row affected (0.00 sec)

SESSION_B>insert into t5 values(5); --被阻塞
^CCtrl-C -- sending "KILL QUERY 93" to server ...
Ctrl-C -- query aborted.
^[[AERROR 1317 (70100): Query execution was interrupted

SESSION_B>insert into t5 values(7); --被阻塞
^CCtrl-C -- sending "KILL QUERY 93" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted

SESSION_B>insert into t5 values(9); --被阻塞
^CCtrl-C -- sending "KILL QUERY 93" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted


SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)


SESSION_A>select * from t5;
+------+
| id   |
+------+
|    1 |
|    4 |
|    7 |
|   10 |
+------+
4 rows in set (0.00 sec)

SESSION_A>commit;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from t5;
+------+
| id   |
+------+
|    1 |
|    2 |
|    4 |
|    7 |
|   10 |
|   12 |
+------+
6 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

當以當前讀模式select * from t5 where id=7 for update;獲取 id=7的資料時,產生了 Next-Key Lock,鎖住了4-10範圍和 id=7單個record
從而阻塞了 SESSION_B在這個範圍內插入資料,而在除此之外的範圍內是可以插入資料的。
在倒數第二個查詢中,因為 read view 的存在,避免了我們看到 2和12兩條資料,避免了幻讀
同時因為 Next-Key Lock 的存在,阻塞了其他回話插入資料,因此當前模式讀不會產生幻讀(select for update 是以當前讀模式獲取資料)

###儘量使用唯一索引,因為唯一索引會把Next-Key Lock降級為Record Lock

實驗6

建立表
(mysql@localhost) [fandb]> create table t6(id int primary key);
Query OK, 0 rows affected (0.02 sec)

SESSION_A>insert into t6 values(1),(4),(7),(10);
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0

開始實驗

SESSION_A>begin;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from t6;
+----+
| id |
+----+
|  1 |
|  4 |
|  7 |
| 10 |
+----+
4 rows in set (0.00 sec)

SESSION_A>select * from t6 where id=7 for update;
+----+
| id |
+----+
|  7 |
+----+
1 row in set (0.00 sec)


SESSION_B>begin;
Query OK, 0 rows affected (0.00 sec)

SESSION_B>insert into t6 values(5); --插入成功沒有阻塞
Query OK, 1 row affected (0.00 sec)

SESSION_B>insert into t6 values(8); --插入成功沒有阻塞
Query OK, 1 row affected (0.00 sec)

SESSION_B>commit;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from t6;
+----+
| id |
+----+
|  1 |
|  4 |
|  7 |
| 10 |
+----+
4 rows in set (0.00 sec)

SESSION_A>commit;
Query OK, 0 rows affected (0.00 sec)

SESSION_A>select * from t6;
+----+
| id |
+----+
|  1 |
|  4 |
|  5 |
|  7 |
|  8 |
| 10 |
+----+
6 rows in set (0.00 sec)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

當 id 列有唯一索引,Next-Key Lock 會降級為 Records Lock