一個鎖定讀取、一個UPDATE
,或一個DELETE
通常會在 SQL 語句處理中掃描的每個索引記錄上設定記錄鎖。語句中是否有會排除該列的 WHERE
條件並不重要。InnoDB
不會記住確切的 WHERE
條件,只知道掃描了哪些索引範圍。鎖通常是下一個索引鍵鎖,也會阻止在記錄前的 “間隙” 中立即插入資料。然而,可以明確地停用間隙鎖定,這會導致不使用下一個索引鍵鎖定。如需更多資訊,請參閱第 17.7.1 節「InnoDB 鎖定」。交易隔離層級也會影響設定哪些鎖;請參閱第 17.7.2.1 節「交易隔離層級」。
如果在搜尋中使用次要索引,且要設定的索引記錄鎖是獨佔的,則 InnoDB
也會擷取對應的叢集索引記錄,並在它們上面設定鎖。
如果沒有適合您語句的索引,且 MySQL 必須掃描整個資料表才能處理該語句,則資料表的每一列都會被鎖定,這反過來會阻止其他使用者對資料表的所有插入操作。建立良好的索引非常重要,這樣您的查詢就不會掃描超過必要的列。
InnoDB
會按如下方式設定特定類型的鎖。
SELECT ... FROM
是一個一致的讀取,讀取資料庫的快照,並且不設定任何鎖,除非交易隔離層級設定為SERIALIZABLE
。對於SERIALIZABLE
層級,搜尋會在它遇到的索引記錄上設定共用下一個索引鍵鎖。但是,對於使用唯一索引搜尋唯一列來鎖定列的語句,只需要一個索引記錄鎖。使用唯一索引的
SELECT ... FOR UPDATE
和SELECT ... FOR SHARE
語句會取得掃描列的鎖,並釋放不符合結果集包含條件的列的鎖 (例如,如果它們不符合WHERE
子句中給定的條件)。但是,在某些情況下,列可能不會立即解鎖,因為在查詢執行期間會遺失結果列及其原始來源之間的關係。例如,在UNION
中,來自資料表的掃描 (和鎖定) 列可能會先插入到暫存資料表中,然後再評估它們是否符合結果集條件。在這種情況下,暫存資料表中的列與原始資料表中的列的關係會遺失,而後者列會一直到查詢執行結束才解鎖。對於鎖定讀取(
SELECT
搭配FOR UPDATE
或FOR SHARE
)、UPDATE
和DELETE
陳述式,所取得的鎖定取決於陳述式是否使用具有唯一搜尋條件的唯一索引,或是範圍類型的搜尋條件。對於具有唯一搜尋條件的唯一索引,
InnoDB
只鎖定找到的索引記錄,而不是其之前的間隙。對於其他搜尋條件和非唯一索引,
InnoDB
會鎖定掃描的索引範圍,使用間隙鎖定或下一鍵鎖定來阻止其他會話將資料插入範圍所涵蓋的間隙中。有關間隙鎖定和下一鍵鎖定的資訊,請參閱第 17.7.1 節,「InnoDB 鎖定」。
對於搜尋遇到的索引記錄,
SELECT ... FOR UPDATE
會阻止其他會話執行SELECT ... FOR SHARE
或在某些交易隔離層級中讀取。一致性讀取會忽略在讀取檢視中存在的記錄上設定的任何鎖定。UPDATE ... WHERE ...
會對搜尋遇到的每個記錄設定獨佔的下一鍵鎖定。但是,對於使用唯一索引來搜尋唯一列的鎖定列陳述式,只需要索引記錄鎖定。當
UPDATE
修改叢集索引記錄時,會在受影響的次要索引記錄上取得隱含鎖定。UPDATE
操作在插入新的次要索引記錄之前執行重複檢查掃描時,以及在插入新的次要索引記錄時,也會在受影響的次要索引記錄上取得共用鎖定。DELETE FROM ... WHERE ...
會對搜尋遇到的每個記錄設定獨佔的下一鍵鎖定。但是,對於使用唯一索引來搜尋唯一列的鎖定列陳述式,只需要索引記錄鎖定。INSERT
會在插入的列上設定獨佔鎖定。此鎖定是索引記錄鎖定,而不是下一鍵鎖定(也就是說,沒有間隙鎖定),並且不會阻止其他會話將資料插入插入列之前的間隙中。在插入列之前,會設定一種稱為插入意圖間隙鎖定的間隙鎖定。這種鎖定會以一種方式發出插入意圖的訊號,如果多個交易在同一個索引間隙中插入,但如果它們不是在間隙內的相同位置插入,則不需要互相等待。假設索引記錄的值為 4 和 7。嘗試插入值 5 和 6 的單獨交易會在取得插入列的獨佔鎖定之前,各自使用插入意圖鎖定來鎖定 4 和 7 之間的間隙,但它們不會互相阻止,因為列之間沒有衝突。
如果發生重複鍵錯誤,則會在重複的索引記錄上設定共用鎖定。如果另一個會話已經擁有獨佔鎖定,則當有多個會話嘗試插入同一列時,使用共用鎖定可能會導致死鎖。如果另一個會話刪除該列,則可能會發生這種情況。假設
InnoDB
表格t1
具有以下結構CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
現在假設三個會話依序執行以下操作
會話 1
START TRANSACTION; INSERT INTO t1 VALUES(1);
會話 2
START TRANSACTION; INSERT INTO t1 VALUES(1);
會話 3
START TRANSACTION; INSERT INTO t1 VALUES(1);
會話 1
ROLLBACK;
會話 1 的第一個操作會取得該列的獨佔鎖定。會話 2 和 3 的操作都會導致重複鍵錯誤,並且它們都會請求該列的共用鎖定。當會話 1 回滾時,它會釋放該列的獨佔鎖定,並且會授予會話 2 和 3 的已排隊共用鎖定請求。此時,會話 2 和 3 死鎖:由於對方持有共用鎖定,因此兩者都無法取得該列的獨佔鎖定。
如果表格已經包含鍵值為 1 的列,並且三個會話依序執行以下操作,則會發生類似的情況
會話 1
START TRANSACTION; DELETE FROM t1 WHERE i = 1;
會話 2
START TRANSACTION; INSERT INTO t1 VALUES(1);
會話 3
START TRANSACTION; INSERT INTO t1 VALUES(1);
會話 1
COMMIT;
會話 1 的第一個操作會取得該列的獨佔鎖定。會話 2 和 3 的操作都會導致重複鍵錯誤,並且它們都會請求該列的共用鎖定。當會話 1 提交時,它會釋放該列的獨佔鎖定,並且會授予會話 2 和 3 的已排隊共用鎖定請求。此時,會話 2 和 3 死鎖:由於對方持有共用鎖定,因此兩者都無法取得該列的獨佔鎖定。
INSERT ... ON DUPLICATE KEY UPDATE
與簡單的INSERT
不同,在發生重複鍵錯誤時,會將獨佔鎖定而不是共用鎖定放在要更新的列上。對於重複的主鍵值,會取得獨佔索引記錄鎖定。對於重複的唯一鍵值,會取得獨佔的下一鍵鎖定。INSERT INTO T SELECT ... FROM S WHERE ...
會對插入T
的每一列設定獨佔索引記錄鎖定(沒有間隙鎖定)。如果交易隔離層級為READ COMMITTED
,則InnoDB
會以一致性讀取的方式對S
執行搜尋(沒有鎖定)。否則,InnoDB
會在S
中的列上設定共用下一鍵鎖定。InnoDB
必須在後者情況下設定鎖定:在使用以陳述式為基礎的二進位記錄進行向前回復期間,每個 SQL 陳述式都必須以與原始方式完全相同的方式執行。CREATE TABLE ... SELECT ...
會執行SELECT
,並帶有共用下一鍵鎖定或作為一致性讀取,與INSERT ... SELECT
相同。當在
REPLACE INTO t SELECT ... FROM s WHERE ...
或UPDATE t ... WHERE col IN (SELECT ... FROM s ...)
建構中使用SELECT
時,InnoDB
會在表格s
中的列上設定共用下一鍵鎖定。InnoDB
會在初始化表格上先前指定的AUTO_INCREMENT
資料行時,在與AUTO_INCREMENT
資料行關聯的索引結尾設定獨佔鎖定。使用
innodb_autoinc_lock_mode=0
,InnoDB
會使用特殊的AUTO-INC
表格鎖定模式,其中在存取自動遞增計數器時,取得並保留鎖定直到目前 SQL 陳述式的結尾(而不是整個交易的結尾)。在持有AUTO-INC
表格鎖定時,其他用戶端無法將資料插入表格中。使用innodb_autoinc_lock_mode=1
的 「大量插入」也會發生相同的行為。表格層級的AUTO-INC
鎖定不會搭配innodb_autoinc_lock_mode=2
使用。如需詳細資訊,請參閱第 17.6.1.6 節,「InnoDB 中的 AUTO_INCREMENT 處理」。InnoDB
會在不設定任何鎖定的情況下,擷取先前初始化的AUTO_INCREMENT
資料行的值。如果在表格上定義了
FOREIGN KEY
約束,則任何需要檢查約束條件的插入、更新或刪除都會在其檢查約束的記錄上設定共用記錄層級的鎖定。InnoDB
也會在約束失敗的情況下設定這些鎖定。LOCK TABLES
會設定表格鎖定,但設定這些鎖定的是高於InnoDB
層的較高 MySQL 層。InnoDB
如果innodb_table_locks = 1
(預設值)且autocommit = 0
,則會知道表格鎖定,而高於InnoDB
的 MySQL 層會知道資料列層級的鎖定。否則,
InnoDB
的自動死鎖偵測無法偵測到涉及此類表格鎖定的死鎖。此外,由於在這種情況下,較高的 MySQL 層不知道資料列層級的鎖定,因此可能會在另一個會話目前具有資料列層級鎖定的表格上取得表格鎖定。但是,這不會危及交易完整性,如第 17.7.5.2 節,「死鎖偵測」中所述。如果
innodb_table_locks=1
(預設值),則LOCK TABLES
會在每個表格上取得兩個鎖定。除了 MySQL 層上的表格鎖定之外,它還會取得InnoDB
表格鎖定。若要避免取得InnoDB
表格鎖定,請設定innodb_table_locks=0
。如果未取得InnoDB
表格鎖定,即使表格的某些記錄正由其他交易鎖定,LOCK TABLES
仍會完成。在 MySQL 8.4 中,
innodb_table_locks=0
對於使用LOCK TABLES ... WRITE
明確鎖定的資料表沒有任何效果。它確實對通過LOCK TABLES ... WRITE
隱式鎖定(例如,通過觸發器)或通過LOCK TABLES ... READ
鎖定讀取或寫入的資料表有效果。當交易提交或中止時,交易持有的所有
InnoDB
鎖定都會被釋放。因此,在autocommit=1
模式下,對InnoDB
資料表調用LOCK TABLES
沒有太大意義,因為獲得的InnoDB
資料表鎖定會立即釋放。您無法在交易中途鎖定其他資料表,因為
LOCK TABLES
會執行隱式的COMMIT
和UNLOCK TABLES
。