文檔版本:8.0
來源:Consistent Nonlocking Reads 、Locking Reads
上一篇:事務隔離級別html
本篇主要介紹InnoDB的快照讀與加鎖讀。
快照讀嚴格來講應該翻譯爲一致的無鎖讀(Consistent Nonlocking Reads),但國內通常都稱做快照讀。在以前的篇章中提到的普通讀
、無鎖讀
和一致讀
等等,實際上講的都是快照讀的概念。mysql
快照讀,指的是InnoDB使用多版本機制,爲一次查詢提供數據庫在特定時間點的快照。查詢能夠看到時間點以前已提交事務產生的變動,看不到未提交或時間點以後提交的。但有個特例,同一事物內快照生成以前產生的變動,也是能夠看到的。這個特性會導致這樣一種奇怪的情形:若是你更新了表的一些行,SELECT
語句能夠看到你作出的變動,但也可能看到某些行的老版本。若是其它事務也更新了這個表的一些行,你所看到的表的狀態可能從未在數據庫中存在過。算法
若是事務的隔離級別是可重複讀(默認級別),同一事務內的全部快照讀都會讀取事務內第一次快照讀時創建的快照。經過提交當前事物和發起新的查詢,能夠刷新快照。sql
而在讀已提交級別下,每次快照讀都會創建最新快照。數據庫
在讀已提交和可重複讀級別下,InnoDB會將SELECT
語句所有轉換爲快照讀。快照讀不會給它讀取的表上任何鎖,所以在快照讀執行的同時,其它事務能夠對錶進行任意修改。
假設你在默認的可重複讀級別下,當你發起一次快照讀(也就是普通的SELECT
語句),InnoDB會給你的事務分配一個時間點:即記錄查詢看到數據庫數據的時間點。若是其它事務在這個時間點以後刪除了一行並提交,你的事務將不會感知到行被刪除,插入和更新同理。安全
數據庫快照機制會應用在事務中的SELECT
語句,但不會應用於DML語句。若是你插入或修改了某些行而後提交,同時另外一個可重複讀事務發起的DELETE
或UPDATE
語句,能夠影響到這些剛剛提交的行,即便不能查詢到它們。若是一個事務對另外一個事務提交的行作更新或刪除操做,這些原本不可見的變動將對前者可見。例如,你可能會遇到以下場景:併發
SELECT COUNT(c1) FROM t1 WHERE c1 = 'xyz'; -- 返回 0:沒有匹配記錄。 DELETE FROM t1 WHERE c1 = 'xyz'; -- 將刪除其它事務最近提交的若干行。 SELECT COUNT(c2) FROM t1 WHERE c2 = 'abc'; -- 返回 0:沒有匹配記錄。 UPDATE t1 SET c2 = 'cba' WHERE c2 = 'abc'; -- 影響 10 行:另外一個事務剛剛提交了10條值爲‘abc’的記錄。 SELECT COUNT(c2) FROM t1 WHERE c2 = 'cba'; -- 返回 10: 這個事務如今能夠看到它剛剛修改的行。
你能夠經過提交事務並執行SELECT
或START TRANSACTION WITH CONSISTENT SNAPSHOT
語句來刷新快照時間點。翻譯
上述的這些機制被稱做多版本併發控制。日誌
在下面的例子中,要想會話A看到會話B插入的行,必須讓會話A和會話B都提交事務,從而使快照時間點刷新到B提交之後。code
Session A Session B SET autocommit=0; SET autocommit=0; 時間線 | SELECT * FROM t; | 空集 | INSERT INTO t VALUES (1, 2); | v SELECT * FROM t; 空集 COMMIT; SELECT * FROM t; 空集 COMMIT; SELECT * FROM t; --------------------- | 1 | 2 | ---------------------
若是你想保證看到數據庫的最新狀態,使用讀已提交級別,亦或發起加鎖讀:
SELECT * FROM t FOR SHARE;
在讀已提交級別下,同一事物內的每一次快照讀都將設置本身的快照。而使用FOR SHARE語句,取而代之是產生一次加鎖讀:SELECT語句將阻塞直至事務得到到最新的行記錄。
注意:FOR SHARE語句並不會刷新可重複讀事務的快照時間點。
快照讀對特定的DDL語句無效:
對於ALTER TABLE操做,MySQL提供三種算法:
- COPY:將在原表的拷貝中進行,隨後表數據將從原表中逐行拷貝。期間不容許DML語句並行。
- INPLACE:不進行拷貝操做,但可能會原地重建表。在準備與執行階段,ALTER操做可能會獲取表的元數據獨佔鎖。期間通常容許DML語句並行。
- INSTANT:只會修改數據字典中的元數據。在準備與執行階段不會獲取表的元數據獨佔鎖,表數據也不會被影響,操做是瞬時的,容許DML語句並行。
當ALTER語句不支持INSTANT和INPLACE時,MySQL纔會採起COPY算法進行拷貝-刪除,例如更改字段順序、改變字段類型或添加一個主鍵等等。關於觸發COPY的各種條件詳見:Online DDL Operations
對於SELECT語句的變種,如未明確FOR UPDATE
或FOR SHARE
的INSERT INTO ... SELECT
,UPDATE ... (SELECT)
和 CREATE TABLE ... SELECT
語句:
若是你在一個事務內先查詢數據,再插入或更新與之相關聯的數據,普通的SELECT語句不會給予足夠的安全性保證。其餘事務能夠更新或刪除你剛剛查詢的行。InnoDB支持兩種類型的加鎖讀來提供額外的安全性保證:
對查詢到的行施加共享鎖。其它事務能夠讀取這些行,但在你的事務提交前不能修改它們。若是其它事務事先修改了這些行而沒有提交,你的查詢將阻塞直至其它事務提交,隨後獲取到最新的值。
注意
SELECT ... FOR SHARE
是SELECT ... LOCK IN SHARE MODE
的替代語法,但LOCK IN SHARE MODE
仍保留下來做向後兼容。語句功能是一致的,但FOR SHARE
支持OF table_name
,NOWAIT
和and SKIP LOCKED
選項。詳見Locking Read Concurrency with NOWAIT and SKIP LOCKED。
對於掃描到的索引值,鎖定其所在行和任何關聯的索引項,這一點與執行UPDATE
語句效果相同。其它事務若是對這些行進行更新、執行SELECT ... FOR SHARE
或特定隔離級別下讀取,將阻塞。若是對快照視圖中存在的行加鎖,快照讀會忽略這些鎖。(老版本的行記錄不能被鎖定,由於它們是經過將行拷貝到內存中並執行回滾日誌來重建的。)
SELECT ... FOR UPDATE
須要SELECT
權限,以及DELETE
,LOCK TABLES
或UPDATE
的至少一種權限。
不管在單表仍是多表中,上述兩種語句在處理樹形結構與圖形結構的數據時很是有用。它們能在遍歷圖邊界或窮舉樹分支的同時,保留了回退與改變「結點」值的權利。
FOR SHARE
和FOR UPDATE
查詢所產生的鎖將在事務提交或回滾後釋放。
注意
加鎖讀只有在自動提交關閉的狀況下才可用。(要麼使用
START TRANSACTION
開啓事務,要麼設置autocommit
爲0。)
聲明在外部語句的加鎖讀不會鎖定嵌套子查詢中的行,除非加鎖讀也聲明在子查詢中。例如,下列語句不會鎖定t2表中的行:
SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2) FOR UPDATE;
若要鎖定t2表中的行,在子查詢中聲明加鎖讀:
SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE;
假設你想在child表插入一行,而且確保child行在parent表有一個父行。你的應用代碼能夠在這個操做序列中的確保參照完整性。
首先,使用快照讀查詢PARENT表來驗證父行是否存在,然而你能安全地將子行插入CHILD表嗎?不行,由於其它事務能夠在你執行SELECT
和INSERT
操做之間刪除該父行,而你沒法感知到。
爲了不這一可能發生的問題,發起帶FOR SHARE
的SELECT
語句:
SELECT * FROM parent WHERE NAME = 'Jones' FOR SHARE;
在FOR SHARE
查詢返回了父行‘Jones’後,你就能夠安全地將子行插入到CHILD表並提交了。任未嘗試獲取PARENT表中可用行的獨佔鎖的事務都會等待,直到你的事務結束,同時也表明每一個表中的數據都處於一致狀態。
舉另外一個例子,在CHILD_NODES表中設有一整形計數字段,用於給每一個插入到CHILD表的子行賦一個惟一標識。在這種狀況下不要使用快照讀或共享鎖來讀取當前計數器的值,由於兩個數據庫用戶可能看到計數器的同一個值,並將在CHILD表插入相同標識值的行,因而會發生重複鍵錯誤。
在這種狀況下,FOR SHARE
不是一個好的解決方案,若是兩個事務同時讀取到相同的計數值,至少會有一個事務在試圖更新計數器時因死鎖而終止。
如何實現對計數器的讀取和迭代?先使用FOR UPDATE
對計數器發起加鎖讀,而後迭代計數器。例如:
SELECT counter_field FROM child_codes FOR UPDATE; UPDATE child_codes SET counter_field = counter_field + 1;
SELECT ... FOR UPDATE
語句讀取最新的可用數據,爲讀取到的行設置獨佔鎖。所以就能夠在隨後的UPDATE
中匹配到的行上設置相同的鎖。
上面的案例只是爲了說明SELECT ... FOR UPDATE
如何工做。在MySQL中,生成惟一標識的工做能夠經過一條語句完成:
UPDATE child_codes SET counter_field = LAST_INSERT_ID(counter_field + 1); SELECT LAST_INSERT_ID();
其中SELECT
語句只是爲了得到標識信息(與當前的數據庫鏈接相關聯),不會查詢任何表。
若是事務鎖住一行數據,其它事務對同一行發起SELECT ... FOR UPDATE
或SELECT ... FOR SHARE
查詢將必須等待鎖被釋放。這一特性防止了那些查詢出來並將被更新的行被其它事務更新或者刪除。然而有時你想要查詢行被上鎖時語句當即返回,或者能夠接受結果集中不包含被上鎖的行時,就沒有必要等待行鎖被釋放。
經過在SELECT ... FOR UPDATE
或SELECT ... FOR SHARE
中設置NOWAIT
與SKIP LOCKED
選項,能夠避免沒必要要的鎖等待。
使用了SKIP LOCKED
的加鎖讀也不會等待獲取行鎖。查詢當即執行,從結果集中剔除被上鎖的行。
注意
跳過加鎖行的查詢將返回一個非一致性的數據視圖。所以
SKIP LOCKED
不適合常規事務場景。但能夠用於在多個事務訪問隊列類型的表時避免鎖競爭。
NOWAIT
和SKIP LOCKED
只能用於行級鎖。
在複製語句中使用NOWAIT
和SKIP LOCKED
是不安全的。
下面演示NOWAIT
和SKIP LOCKED
如何使用。會話1開啓了一個事務並獲取了一行行鎖。會話2在同一行發起附帶NOWAIT
選項的加鎖讀,由於請求行被會話1鎖住,加鎖讀馬上返回了錯誤。會話3發起附帶SKIP LOCKED
的加鎖讀,則返回了不包含會話1鎖住行的結果集。
# 會話 1: mysql> CREATE TABLE t (i INT, PRIMARY KEY (i)) ENGINE = InnoDB; mysql> INSERT INTO t (i) VALUES(1),(2),(3); mysql> START TRANSACTION; mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE; +---+ | i | +---+ | 2 | +---+ # 會話 2: mysql> START TRANSACTION; mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE NOWAIT; ERROR 3572 (HY000): Do not wait for lock. # 會話 3: mysql> START TRANSACTION; mysql> SELECT * FROM t FOR UPDATE SKIP LOCKED; +---+ | i | +---+ | 1 | | 3 | +---+