快照讀與加鎖讀 - MySQL 8.0官方文檔筆記(三)

文檔版本:8.0
來源:Consistent Nonlocking ReadsLocking Reads
上一篇:事務隔離級別html

本篇主要介紹InnoDB的快照讀與加鎖讀。
快照讀嚴格來講應該翻譯爲一致的無鎖讀(Consistent Nonlocking Reads),但國內通常都稱做快照讀。在以前的篇章中提到的普通讀無鎖讀一致讀等等,實際上講的都是快照讀的概念。mysql

快照讀

快照讀,指的是InnoDB使用多版本機制,爲一次查詢提供數據庫在特定時間點的快照。查詢能夠看到時間點以前已提交事務產生的變動,看不到未提交或時間點以後提交的。但有個特例,同一事物內快照生成以前產生的變動,也是能夠看到的。這個特性會導致這樣一種奇怪的情形:若是你更新了表的一些行,SELECT語句能夠看到你作出的變動,但也可能看到某些行的老版本。若是其它事務也更新了這個表的一些行,你所看到的表的狀態可能從未在數據庫中存在過。算法

若是事務的隔離級別是可重複讀(默認級別),同一事務內的全部快照讀都會讀取事務內第一次快照讀時創建的快照。經過提交當前事物和發起新的查詢,能夠刷新快照。sql

而在讀已提交級別下,每次快照讀都會創建最新快照。數據庫

在讀已提交和可重複讀級別下,InnoDB會將SELECT語句所有轉換爲快照讀。快照讀不會給它讀取的表上任何鎖,所以在快照讀執行的同時,其它事務能夠對錶進行任意修改。
假設你在默認的可重複讀級別下,當你發起一次快照讀(也就是普通的SELECT語句),InnoDB會給你的事務分配一個時間點:即記錄查詢看到數據庫數據的時間點。若是其它事務在這個時間點以後刪除了一行並提交,你的事務將不會感知到行被刪除,插入和更新同理。安全

注意

數據庫快照機制會應用在事務中的SELECT語句,但不會應用於DML語句。若是你插入或修改了某些行而後提交,同時另外一個可重複讀事務發起的DELETEUPDATE語句,能夠影響到這些剛剛提交的行,即便不能查詢到它們。若是一個事務對另外一個事務提交的行作更新或刪除操做,這些原本不可見的變動將對前者可見。例如,你可能會遇到以下場景:併發

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: 這個事務如今能夠看到它剛剛修改的行。

你能夠經過提交事務並執行SELECTSTART 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語句無效:

  • 快照讀對DROP TABLE無效,由於MySQL不能使用一張被釋放的表,InnoDB已經銷燬了表。
  • 快照讀對進行了拷貝-刪除操做的ALTER TABLE無效。當從新發起一次快照讀時,事務將看不到新表中的行由於在快照生成時這些行還不存在。此時,事務會返回一個錯誤:ER_TABLE_DEF_CHANGED,「表定義已改變,請重試事務」。

對於ALTER TABLE操做,MySQL提供三種算法:

  • COPY:將在原表的拷貝中進行,隨後表數據將從原表中逐行拷貝。期間不容許DML語句並行。
  • INPLACE:不進行拷貝操做,但可能會原地重建表。在準備與執行階段,ALTER操做可能會獲取表的元數據獨佔鎖。期間通常容許DML語句並行。
  • INSTANT:只會修改數據字典中的元數據。在準備與執行階段不會獲取表的元數據獨佔鎖,表數據也不會被影響,操做是瞬時的,容許DML語句並行。

當ALTER語句不支持INSTANT和INPLACE時,MySQL纔會採起COPY算法進行拷貝-刪除,例如更改字段順序、改變字段類型或添加一個主鍵等等。關於觸發COPY的各種條件詳見:Online DDL Operations

對於SELECT語句的變種,如未明確FOR UPDATEFOR SHAREINSERT INTO ... SELECTUPDATE ... (SELECT)CREATE TABLE ... SELECT語句:

  • 默認狀況下,InnoDB對這些語句使用更強的鎖,而SELECT部分則表現得像讀已提交級別,即每一次快照讀都會設置最新的快照,即便在同一事務內。
  • 若要在這種狀況下進行無鎖讀,須將事務設置爲讀未提交或讀已提交,以免讀行時上鎖。

加鎖讀

若是你在一個事務內先查詢數據,再插入或更新與之相關聯的數據,普通的SELECT語句不會給予足夠的安全性保證。其餘事務能夠更新或刪除你剛剛查詢的行。InnoDB支持兩種類型的加鎖讀來提供額外的安全性保證:

  • SELECT ... FOR SHARE

    對查詢到的行施加共享鎖。其它事務能夠讀取這些行,但在你的事務提交前不能修改它們。若是其它事務事先修改了這些行而沒有提交,你的查詢將阻塞直至其它事務提交,隨後獲取到最新的值。

    注意

    SELECT ... FOR SHARESELECT ... LOCK IN SHARE MODE的替代語法,但LOCK IN SHARE MODE仍保留下來做向後兼容。語句功能是一致的,但FOR SHARE支持OF table_nameNOWAITand SKIP LOCKED選項。詳見Locking Read Concurrency with NOWAIT and SKIP LOCKED

  • SELECT ... FOR UPDATE

    對於掃描到的索引值,鎖定其所在行和任何關聯的索引項,這一點與執行UPDATE語句效果相同。其它事務若是對這些行進行更新、執行SELECT ... FOR SHARE或特定隔離級別下讀取,將阻塞。若是對快照視圖中存在的行加鎖,快照讀會忽略這些鎖。(老版本的行記錄不能被鎖定,由於它們是經過將行拷貝到內存中並執行回滾日誌來重建的。)

    SELECT ... FOR UPDATE須要SELECT權限,以及DELETELOCK TABLESUPDATE的至少一種權限。

    不管在單表仍是多表中,上述兩種語句在處理樹形結構與圖形結構的數據時很是有用。它們能在遍歷圖邊界或窮舉樹分支的同時,保留了回退與改變「結點」值的權利。

    FOR SHAREFOR 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表嗎?不行,由於其它事務能夠在你執行SELECTINSERT操做之間刪除該父行,而你沒法感知到。

爲了不這一可能發生的問題,發起帶FOR SHARESELECT語句:

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語句只是爲了得到標識信息(與當前的數據庫鏈接相關聯),不會查詢任何表。

使用NOWAIT和SKIP LOCKED調整加鎖讀併發性

若是事務鎖住一行數據,其它事務對同一行發起SELECT ... FOR UPDATESELECT ... FOR SHARE查詢將必須等待鎖被釋放。這一特性防止了那些查詢出來並將被更新的行被其它事務更新或者刪除。然而有時你想要查詢行被上鎖時語句當即返回,或者能夠接受結果集中不包含被上鎖的行時,就沒有必要等待行鎖被釋放。

經過在SELECT ... FOR UPDATESELECT ... FOR SHARE中設置NOWAITSKIP LOCKED選項,能夠避免沒必要要的鎖等待。

  • NOWAIT

    使用了`NOWAIT`的加鎖讀將不會等待獲取行鎖。查詢當即執行,在行被上鎖時返回一個錯誤。
  • SKIP LOCKED

    使用了SKIP LOCKED的加鎖讀也不會等待獲取行鎖。查詢當即執行,從結果集中剔除被上鎖的行。

    注意

    跳過加鎖行的查詢將返回一個非一致性的數據視圖。所以SKIP LOCKED不適合常規事務場景。但能夠用於在多個事務訪問隊列類型的表時避免鎖競爭。

NOWAITSKIP LOCKED只能用於行級鎖。

在複製語句中使用NOWAITSKIP LOCKED是不安全的。

下面演示NOWAITSKIP 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 |
+---+
相關文章
相關標籤/搜索