InnoDB的鎖機制淺析(三)—幻讀

文章總共分爲五個部分:html

大而全版(五合一):InnoDB的鎖機制淺析(All in One)mysql

前言

這一章節,咱們經過幻讀,逐步展開對InnoDB鎖的探究。sql

1 幻讀概念

解釋了不一樣概念的鎖的做用域,咱們來看一下幻讀究竟是什麼。幻讀在RR條件下是不會出現的。由於RR是Repeatable Read,它是一種事務的隔離級別,直譯過來也就是「在同一個事務中,一樣的查詢語句的讀取是可重複」,也就是說他不會讀到」幻影行」(其餘事務已經提交的變動),它讀到的只能是重複的(不管在第一次查詢以後其餘事務作了什麼操做,第二次查詢結果與第一次相同)。數據庫

上面的例子都是使用for update,這種讀取操做叫作當前讀,對於普通的select語句均爲快照讀。併發

當前讀,又叫加鎖讀,或者 阻塞讀。這種讀取操做再也不是讀取快照,而是讀取最新版本而且加鎖。
快照讀不會添加任何鎖。ui

官方文檔對於幻讀的定義是這樣的:翻譯

原文: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語句執行了兩次,第二次執行的時候比第一次執行時多出一行,則該行就是所謂的幻影行。設計

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times.,這句話看起來應該是不可重複讀的定義,一樣的查詢獲得了不一樣的結果(兩次結果不是重複的),可是後面的舉例給出了幻讀真正的定義,第二次比第一次多出了一行。也就是說,幻讀的出現有這樣一個前提,第二次查詢前其餘事務提交了一個INSERT插入語句。而不可重複讀出現的前提是第二次查詢前其餘事務提交了UPDATE或者DELETE操做。code

mysql的快照讀,使得在RR的隔離級別上在next-Key的做用區間內,製造了一個快照副本,這個副本是隔離的,不管副本對應的區間裏的數據被其餘事務如何修改,在當前事務中,取到的數據永遠是副本中的數據。
RR級別下之因此能夠讀到以前版本的數據,是因爲數據庫的MVCC(Multi-Version Concurrency Control,多版本併發控制)。參見InnoDB Multi-Versioninghtm

有些文章中提到「RR也不能徹底避免幻讀」,實際上官方文檔實際要表達的意義是「在同一個事務內,屢次連續查詢的結果是同樣的,不會因其餘事務的修改而致使不一樣的查詢結果」,這裏先給出實驗結論:

1.當前事務若是未發生更新操做(增刪改),快照版本會保持不變,屢次查詢讀取的副本是同一個。
2.當前事務若是發生更新(增刪改),再次查詢時,會刷新快照版本。


示例的基礎是一個只有兩列的數據庫表。

mysql> CREATE TABLE test (
id int(11) NOT NULL,
code int(11) NOT NULL, 
PRIMARY KEY(id), 
KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 

mysql> INSERT INTO test(id,code) values(1,1),(10,10);

2 RC級別下的幻讀

RC狀況下會出現幻讀。
首先設置隔離級別爲RC,SET SESSION tx_isolation='READ-COMMITTED';

事務一 事務二
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)隔離級別能夠避免髒讀,事務內沒法獲取其餘事務未提交的變動,可是因爲可以讀到已經提交的事務,所以會出現幻讀和不重複讀。
也就是說,RC的快照讀是讀取最新版本數據,而RR的快照讀是讀取被next-key鎖做用區域的副本

3 RR級別下可否避免幻讀?

咱們先來模擬一下RR隔離級別下沒有出現幻讀的狀況:

開啓第一個事務並執行一次快照查詢。

事務一 事務二
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)

這兩個事務的執行,有兩個問題:

1.爲何以前的例子中,在第二個事務的INSERT被阻塞了,而此次卻執行成功了。
這是由於原來的語句中帶有for update,這種讀取是當前讀,會加鎖。而本次第一個事務中的SELECT僅僅是快照讀,沒有加任何鎖。因此不會阻塞其餘的插入。

2.數據庫中的數據已經改變,爲何會讀不到?
這個就是以前提到的next-key lock鎖定的副本。RC及如下級別纔會讀到已經提交的事務。更多的業務邏輯是但願在某段時間內或者某個特定的邏輯區間中,先後查詢到的數據是一致的,當前事務是和其餘事務隔離的。這也是數據庫在設計實現時遵循的ACID原則。

再給出RR條件下出現幻讀的情形,這種情形不須要兩個事務,一個事務就已經能夠說明,

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where id>8;
+----+------+
| id | code |
+----+------+
| 10 |   10 |
+----+------+
1 row in set (0.00 sec)

mysql> update test set code=9 where id=10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from test where id>8;
+----+------+
| id | code |
+----+------+
| 10 |    9 |
+----+------+
1 row in set (0.00 sec)

至於RR隔離級別下到底會不會出現幻讀,就須要看幻讀的定義中的查詢究竟是連續的查詢仍是不連續的查詢。若是認爲RR級別下可能會出現幻讀,那該級別下也會出現不重複讀。


RR隔離級別下,雖然不會出現幻讀,可是會所以產生其餘的問題。
前提:當前數據表中只存在(1,1),(5,5),(10,10)三組數據。

若是數據庫隔離級別不是默認,能夠執行SET SESSION tx_isolation='REPEATABLE-READ';(該語句不是全局設置)更新爲RR。

而後執行下列操做:

事務一 事務二 備註
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)
開啓事務一,並查詢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)
開啓第二個事務,插入(11,11)並提交
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)
事務一再查詢一次,因爲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'
事務一明明沒有查到,卻插入不了

4 更新丟失(Lost Update)

4.1 更新丟失

除了上述這類問題外,RR還會有丟失更新的問題。
以下表給出的操做:

事務一 事務二 備註
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)
開啓事務一,並查詢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)
開啓第二個事務,將(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)
事務一再次查詢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條

這個例子裏,事務一的更新是無效的,儘管在這個事務里程序認爲還存在(10,10)記錄。
事務一中更新以前的SELECT操做是快照讀,因此讀到了快照裏的(10,10),而UPDATE中的WHERE子句是當前讀,取得是最新版本的數據,因此matched: 0 Changed: 0

若是上述例子中的操做是對同一條記錄作修改,就會引發更新丟失。例如,事務一和二同時開啓,事務一先執行update test set code=100 where id=10;,事務二再執行update test set code=200 where id=10;,事務一的更新就會被覆蓋。

這就是經典的丟失更新問題,英文叫Lost Update,又叫提交覆蓋,由於是最後執行更新的事務提交致使的覆蓋。還有一種更新丟失叫作回滾覆蓋,即一個事務的回滾把另外一個事務提交的數據給回滾覆蓋了,可是目前市面上全部的數據庫都不支持這種stupid的操做,所以再也不詳述。

4.2 樂觀鎖與悲觀鎖

這種狀況下,引入咱們常見的兩種方式來解決該問題

  • 樂觀鎖:在UPDATEWHERE子句中加入版本號信息來肯定修改是否生效
  • 悲觀鎖:在UPDATE執行前,SELECT後面加上FOR UPDATE來給記錄加鎖,保證記錄在UPDATE前不被修改。SELECT ... FOR UPDATE是加上了X鎖,也能夠經過SELECT ... LOCK IN SHARE MODE加上S鎖,來防止其餘事務對該行的修改。

不管是樂觀鎖仍是悲觀鎖,使用的思想都是一致的,那就是當前讀。樂觀鎖利用當前讀判斷是不是最新版本,悲觀鎖利用當前讀鎖定行。 可是使用樂觀鎖時仍然須要很是謹慎,由於RR是可重複讀的,必定不能在UPDATE以前先把版本號使用快照讀獲取出來。

相關文章
相關標籤/搜索