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


文章總共分為五個部分:

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

前言

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

1 幻讀概念

解釋了不同概念的鎖的作用域,我們來看一下幻讀到底是什么。幻讀在RR條件下是不會出現的。因為RR是Repeatable Read,它是一種事務的隔離級別,直譯過來也就是“在同一個事務中,同樣的查詢語句的讀取是可重復”,也就是說他不會讀到”幻影行”(其他事務已經提交的變更),它讀到的只能是重復的(無論在第一次查詢之后其他事務做了什么操作,第二次查詢結果與第一次相同)。

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

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

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

原文: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操作。

mysql的快照讀,使得在RR的隔離級別上在next-Key的作用區間內,制造了一個快照副本,這個副本是隔離的,無論副本對應的區間里的數據被其他事務如何修改,在當前事務中,取到的數據永遠是副本中的數據。
RR級別下之所以可以讀到之前版本的數據,是由於數據庫的MVCC(Multi-Version Concurrency Control,多版本並發控制)。參見InnoDB Multi-Versioning

有些文章中提到“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之前先把版本號使用快照讀獲取出來。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM