如果您查詢資料,然後在同一個交易中插入或更新相關資料,則常規的 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
查詢設定的所有鎖定。
只有在停用自動提交 (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
之間,其他工作階段可能會刪除父資料列,而您並不會知道。
為了避免這個潛在的問題,請使用 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 |
+---+