1. 程式人生 > >MySQL 8.0 新特性之 InnoDB 鎖定讀取選項

MySQL 8.0 新特性之 InnoDB 鎖定讀取選項

文章目錄

原文地址:MySQL 8.0 Reference Manual

鎖定讀取語句

如果在同一個事務中,先查詢資料,然後再插入或修改相關的資料,普通的 SELECT 語句無法提供足夠的資料保護。其他的事務可能會更新或刪除該事務剛剛查詢過的資料。InnoDB 提供了兩種鎖定讀取(locking read)語句,能夠提供更高的安全性:

  • SELECT … FOR SHARE

    在讀取的行上設定一個共享鎖。其他的會話可以讀取這些行,但是在當前事務提交之前不能修改這些資料。如果其他事務修改了當前查詢需要讀取的任何資料,並且沒有提交,查詢需要等待其他事務結束,然後返回最新的資料。

    注意 is a replacement for
    SELECT … FOR SHARE 替代了之前的 SELECT … LOCK IN SHARE MODE,但是為了後向相容,仍然支援 LOCK IN SHARE MODE。這兩個語句是等價的。不過,FOR SHARE 支援 OF table_name、NOWAIT 以及 SKIP LOCKED 選項。參見後文。

  • SELECT … FOR UPDATE

    對於查詢涉及的索引記錄,鎖定相應的資料行和索引項,效果和執行 UPDATE 語句一樣。其他事務如果需要更新這些行,或者執行這些資料上的 SELECT … FOR SHARE,或者使用某些事務隔離級別讀取這些資料,都會被阻塞。一致性讀將會忽略讀檢視(read view)中存在的資料記錄上的鎖。(記錄的舊版本資料不能被鎖定;它們通過記錄在記憶體中的拷貝,加上回滾日誌進行重構。)

這些選項主要用於處理樹狀結構或圖結構的資料,可以是單個表或跨多個表儲存的資料。可以通過圖的邊界或樹的分支從一個位置移動到另一個位置,同時可以反向移動並修改這些“指標”資料。

事務提交或者回滾時,釋放 FOR SHARE 和 FOR UPDATE 設定的所有鎖。

注意
鎖定讀取只有在禁用了 autocommit 時才會生效(使用 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 表插入一個行新的資料,並且想要確保在 parent 表中存在對應的父級資料。應用程式可以通過以下系列操作確保參照完整性。

首先,執行一個一致性讀取,查詢 PARENT 表並驗證存在相應的父級行。然後可以安全地在 CHILD 表中插入子記錄嗎?不可以,因為其他的會話可能在你的 SELECT 語句和 INSERT 語句之間刪除了父記錄,而你並不知道。

為了避免這個潛在的問題,可以執行以下 SELECT … FOR SHARE 語句:

SELECT * FROM parent WHERE NAME = 'Jones' FOR SHARE;

在 FOR SHARE 語句返回父記錄 ‘Jones’ 之後,你可以安全地在 CHILD 表中增加子記錄,並且提交事務。任何相應獲取 PARENT 表中該記錄上的排他鎖的事務都需要等待你的事務結束,也就是等待所有表中的資料到達一致性的狀態。

再看一個示例,CHILD_CODES 表中存在一個整數計數器欄位,用於為 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 UPDATE 或者 SELECT … FOR SHARE 事務必須等待其他事務釋放資料行上的鎖。這種行為可以防止事務更新或刪除其他事務正在查詢更新的資料行。不過,如果在請求的資料行已經被鎖定的情況下希望立即返回,或者可以接受只返回沒有被鎖定的資料行,就不需要一直等待其他事務釋放行鎖。

為了避免需要等待其他事務釋放鎖,可以為鎖定讀取語句 SELECT … FOR UPDATE 或 SELECT … FOR SHARE 增加 NOWAIT 和 SKIP LOCKED 選項。

  • NOWAIT

    使用 NOWAIT 選項的鎖定讀取不會等待獲取行鎖。查詢立即執行,如果請求的行被鎖定,返回一個錯誤資訊。

  • SKIP LOCKED

    使用 SKIP LOCKED 選項的鎖定讀取不會等待獲取行鎖。查詢立即執行,並且從結果中排除了被鎖定的行。

注意
跳過鎖定行的查詢返回資料的不一致性檢視。因此,SKIP LOCKED 不適合用於常見的交易系統。但是,它可以用於避免多個會話訪問同一個隊列表時的鎖競爭。

NOWAIT 和 SKIP LOCKED 選項只適用於行級鎖。

使用 NOWAIT 或 SKIP LOCKED 選項的鎖定讀取對於基於語句的複製而言並不安全。

以下示例演示了 NOWAIT 和 SKIP LOCKED 選項的使用。會話 1 開始了一個事務,獲取了單個記錄上的行鎖。會話 2 使用 NOWAIT 選項嘗試針對同一條記錄的帶鎖讀取操作。由於請求的資料已經被會話 1 鎖定,會話 2 中的鎖定讀取操作立即返回一個錯誤。在會話 3中,使用鎖定讀取的 SKIP LOCKED 選項返回沒有被會話 1 鎖定的其他資料。

# Session 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 |
+---+

# Session 2:

mysql> START TRANSACTION;

mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE NOWAIT;
ERROR 3572 (HY000): Do not wait for lock.

# Session 3:

mysql> START TRANSACTION;

mysql> SELECT * FROM t FOR UPDATE SKIP LOCKED;
+---+
| i |
+---+
| 1 |
| 3 |
+---+

人生本來短暫,你又何必匆匆!點個贊再走吧!