文章總共分為五個部分:
- InnoDB的鎖機制淺析(一)—基本概念/兼容矩陣
- InnoDB的鎖機制淺析(二)—探索InnoDB中的鎖(Record鎖/Gap鎖/Next-key鎖/插入意向鎖)
- InnoDB的鎖機制淺析(三)—幻讀
- InnoDB的鎖機制淺析(四)—不同SQL的加鎖狀況
- InnoDB的鎖機制淺析(五)—死鎖場景(Insert死鎖)
大而全版(五合一):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 樂觀鎖與悲觀鎖
這種情況下,引入我們常見的兩種方式來解決該問題
- 樂觀鎖:在
UPDATE
的WHERE
子句中加入版本號信息來確定修改是否生效 - 悲觀鎖:在
UPDATE
執行前,SELECT
后面加上FOR UPDATE
來給記錄加鎖,保證記錄在UPDATE
前不被修改。SELECT ... FOR UPDATE
是加上了X鎖,也可以通過SELECT ... LOCK IN SHARE MODE
加上S鎖,來防止其他事務對該行的修改。
無論是樂觀鎖還是悲觀鎖,使用的思想都是一致的,那就是當前讀。樂觀鎖利用當前讀
判斷是否是最新版本,悲觀鎖利用當前讀
鎖定行。
但是使用樂觀鎖時仍然需要非常謹慎,因為RR是可重復讀的,一定不能在UPDATE之前先把版本號使用快照讀獲取出來。