如果您查詢資料,然後在同一個交易中插入或更新相關資料,則常規的 SELECT
陳述式提供的保護不足。其他交易可以更新或刪除您剛查詢的相同列。InnoDB
支援兩種提供額外安全性的鎖定讀取類型。
在任何讀取的列上設定共用模式鎖定。其他工作階段可以讀取這些列,但在您的交易提交之前無法修改它們。如果這些列中的任何一列被另一個尚未提交的交易更改,您的查詢會等待該交易結束,然後使用最新的值。
注意SELECT ... FOR SHARE
是SELECT ... LOCK IN SHARE MODE
的替代方案,但LOCK IN SHARE MODE
仍可向後相容。這些陳述式是等效的。但是,FOR SHARE
支援OF
、table_name
NOWAIT
和SKIP LOCKED
選項。請參閱使用 NOWAIT 和 SKIP LOCKED 的鎖定讀取並行性。SELECT ... FOR SHARE
需要SELECT
權限。SELECT ... FOR SHARE
陳述式不會在 MySQL 授權表上取得讀取鎖定。如需詳細資訊,請參閱授權表並行性。對於搜尋遇到的索引記錄,會鎖定這些列和任何相關的索引項目,就像您為這些列發出
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
中插入新的資料列,並確保子資料列在表格 parent
中有父資料列。您的應用程式程式碼可以在此操作序列中確保參照完整性。
首先,使用一致性讀取來查詢表格 PARENT
並驗證父資料列是否存在。您可以安全地將子資料列插入表格 CHILD
嗎?不行,因為在您的 SELECT
和 INSERT
之間,其他會話可能會刪除父資料列,而您卻不知道。
為了避免這個潛在問題,請使用 FOR SHARE
執行 SELECT
。
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
讀取最新的可用資料,並在讀取的每個資料列上設定獨佔鎖定。因此,它會設定與搜尋過的 SQL 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
交易必須等待,直到封鎖交易釋放資料列鎖定。這種行為可防止交易更新或刪除其他交易查詢以進行更新的資料列。但是,如果您希望查詢在請求的資料列被鎖定時立即傳回,或者如果從結果集中排除鎖定的資料列是可以接受的,則無需等待資料列鎖定被釋放。
為了避免等待其他交易釋放資料列鎖定,NOWAIT
和 SKIP LOCKED
選項可以與 SELECT ... FOR UPDATE
或 SELECT ... FOR SHARE
鎖定讀取語句一起使用。
NOWAIT
使用
NOWAIT
的鎖定讀取永遠不會等待取得資料列鎖定。查詢會立即執行,如果請求的資料列被鎖定,則會失敗並出現錯誤。SKIP LOCKED
使用
SKIP LOCKED
的鎖定讀取永遠不會等待取得資料列鎖定。查詢會立即執行,並從結果集中移除鎖定的資料列。注意跳過鎖定資料列的查詢會傳回不一致的資料視圖。
SKIP LOCKED
因此不適用於一般交易工作。但是,它可用於避免多個會話存取相同的類佇列表格時發生鎖定爭用。
NOWAIT
和 SKIP LOCKED
僅適用於資料列層級鎖定。
使用 NOWAIT
或 SKIP LOCKED
的語句對於基於語句的複製是不安全的。
以下範例示範 NOWAIT
和 SKIP LOCKED
。會話 1 開始一個在單一記錄上取得資料列鎖定的交易。會話 2 嘗試使用 NOWAIT
選項對同一記錄執行鎖定讀取。由於請求的資料列被會話 1 鎖定,鎖定讀取會立即傳回錯誤。在會話 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 |
+---+