【MySQL技術內幕】36-一致性的非鎖定讀
一致性的非鎖定讀( consistent nonlocking read)是指 InnoDB儲存引擎通過行多版本控制(multi versioning)的方式來讀取當前執行時間資料庫中行的資料。如果讀取的行正在執行 DELETE或 UPDATE操作,這時讀取操作不會因此去等待行上鎖的釋放。相反地, InnoDB儲存引擎會去讀取行的一個快照資料。如圖所示。 圖直觀地展現了 InnoDB儲存引擎一致性的非鎖定讀。之所以稱其為非鎖定讀,因為不需要等待訪問的行上X鎖的釋放。 快照資料是指該行的之前版本的資料,該實現是通過undo段來完成。而 undo用來在事務中回滾資料,因此快照資料本身是沒有額外的開銷。此外,讀取快照資料是不需要上鎖的,因為沒有事務需要對歷史的資料進行修改操作。 可以看到,非鎖定讀機制極大地提高了資料庫的併發性。在 InnoDB儲存引擎的預設設定下,這是預設的讀取方式,即讀取不會佔用和等待表上的鎖。但是在不同事務隔離級別下,讀取的方式不同,並不是在每個事務隔離級別下都是採用非鎖定的一致性讀。此外,即使都是使用非鎖定的一致性讀,但是對於快照資料的定義也各不相同。 通過圖6-4可以知道,快照資料其實就是當前行資料之前的歷史版本,每行記錄可能有多個版本。就圖6-4所顯示的,一個行記錄可能有不止一個快照資料,一般稱這種技術為行多版本技術。由此帶來的併發控制,稱之為多版本併發控制(Multi version Concurrency Control, MVCC)。 在事務隔離級別 READ COMMITTED和 REPEATABLE READ( InnoDB儲存引擎的預設事務隔離級別)下, InnoDB儲存引擎使用非鎖定的一致性讀。然而,對於快照資料的定義卻不相同。在 READ COMMITTED事務隔離級別下,對於快照資料,非一致性讀總是讀取被鎖定行的最新一份快照資料。而在REPEATABLE READ事務隔離級別下,對於快照資料,非一致性讀總是讀取事務開始時的行資料版本。來看下面的一個例子,首先在當前 MySQL資料庫的連線會話A中執行如下SQL語句:
#Session A
mysql> BEGIN;
Query OK,0 rows affected (0.00 sec)
mysql> SELECT * FROM parent WHERE id =1;
id
1
會話A中已通過顯式地執行命令 BEGIN開啟了一個事務,並讀取了表 parent中id為1的資料,但是事務並沒有結束。與此同時,使用者再開啟另一個會話B,這樣可以模擬併發的情況,然後對會話B做如下的操作
#Session B mysql> BEGIN; Query OK, 0 rows affected (0.00 sec) mysql> UPDATE parent set id=3 WHERE id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
在會話B中將事務表 parent中i為1的記錄修改為id=3,但是事務同樣沒有提交,這樣id=1的行其實加了一個X鎖。這時如果在會話A中再次讀取id為1的記錄,根據 InnoDB儲存引擎的特性,即在 READ COMMITTED和 REPEATETABLE READ的事務隔離級別下會使用非鎖定的一致性讀。回到之前的會話A,接著上次未提交的事務,執行SQL語句 SELECT* FROM parent WHERE id=1的操作,這時不管使用READ COMMITTED還是 REPEATABLE READ的事務隔離級別,顯示的資料應該都是:
#Session A mysql> SELECT FROM parent WHERE id = 1; id 1 1 row in set (0.00 sec)
由於當前id=1的資料被修改了1次,因此只有一個行版本的記錄。接著,在會話B中提交上次的事務:
# Session B
mysql> commit
Query OK, 0 rows affected (0.01 sec)
在會話B提交事務後,這時在會話A中再執行 SELECT * FROM parent WHERE id=1的SQL語句,在READ COMMITTED和REPEATABLE事務隔離級別下得到結果就不一樣了。對於READ COMMITTED的事務隔離級別,它總是讀取行的最新版本,如果行被鎖定了,則讀取該行版本的最新一個快照(fresh snapshot)。在上述例子中,因為會話B已經提交了事務,所以READ COMMITTED事務隔離級別下會得到如下結果:
mysql> SELECT @@tx_isolation\G;
******************1.row********************
@@tx_isolation: READ-COMMITTED
l row in set (0.00 sec)
mysql> SELECT FROM parent WHERE id =1;
Empty set (0.00 sec)
而對於 REPEATABLE READ的事務隔離級別,總是讀取事務開始時的行資料。因此對於REPEATABLE READ事務隔離級別,其得到的結果如下:
mysql> SELECT @@tx_isolation\G;
******************1.row********************
@@tx_isolation:: REPEATABLE-READ
1 row in set (0.00 sec)
mysql> SELECT FROM parent WHERE id = 1;
id
1
1 row in set (0.00 sec)
下面將從時間的角度展現上述演示的示例過程,如下表所示。需要特別注意的是,對於READ COMMITTED的事務隔離級別而言,從資料庫理論的角度來看,其違反了事務ACID中的I的特性,即隔離性。
時間 | 會話A | 會話B |
1 | BEGIN | |
2 | SELECT * FROM parent WHERE id= 1 | |
3 | BEGIN | |
4 | UPDATE parent SET id=3 WHERE id=1 | |
5 | SELECT * FROM parent WHERE id=1 | |
6 | COMMIT | |
7 | SELECT * FROM parent WHERE id=1; | |
8 | COMMIT |