1. 程式人生 > >技術分享 | 修改外來鍵和元資料鎖定

技術分享 | 修改外來鍵和元資料鎖定

原創: 管長龍 譯


原文:https://www.percona.com/blog/2019/07/02/alters-foreign-keys-and-metadata-locks-oh-my/ 作者:Mike Benshoof

當你開始執行一個 ALTER ,而你遇到了可怕的“元資料鎖定等待”,我敢肯定你一定遇見過。我最近遇到了一個案例,其中被更改的表要執行一個很小範圍的更新(<100行)。ALTER 在負載測試期間一直等待了幾個小時。在停止負載測試後,ALTER 按預期在不到一秒的時間內就完成了。那麼這裡發生了什麼?

檢查外來鍵

每當有奇數次鎖定時,我的第一直覺就是檢查外來鍵。當然這張表有一些外來鍵引用了一個更繁忙的表。但是這種行為似乎仍然很奇怪。對錶執行 ALTER 時,會針對子表請求一個 SHARED_UPGRADEABLE 元資料鎖。還有針對父級的 SHARED_READ_ONLY 元資料鎖。

我們來看看如何根據文件獲取元資料鎖定[1]:

如果給定鎖定有多個伺服器,則首先滿足最高優先順序鎖定請求,並且與 max_write_lock_count系統變數有關。寫鎖定請求的優先順序高於讀取鎖定請求。

[1]:https://dev.mysql.com/doc/refman/en/metadata-locking.html

請務必注意鎖定順序是序列化的:語句逐個獲取元資料鎖,而不是同時獲取,並在此過程中執行死鎖檢測。

通常在考慮佇列時考慮先進先出。如果我發出以下三個語句(按此順序),它們將按以下順序完成:

1. INSERT INTO parent 

2. ALTER TABLE child

3. INSERT INTO parent

但是當子 ALTER 語句請求對父進行讀取鎖定時,儘管排序,但兩個插入將在 ALTER 之前完成。以下是可以演示此示例的示例場景:

資料初始化:

CREATE TABLE `parent` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`val` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE TABLE `child` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) DEFAULT NULL,
`val` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_parent` (`parent_id`),
CONSTRAINT `fk_parent` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB;
INSERT INTO `parent` VALUES (1, "one"), (2, "two"), (3, "three"), (4, "four");

Session 1:

start transaction;
update parent set val = "four-new" where id = 4;

Session 2:

alter table child add index `idx_new` (val);

Session 3:

start transaction;
update parent set val = "three-new" where id = 3;

此時,會話 1 具有開啟的事務,並且處於休眠狀態,並在父級上授予寫入元資料鎖定。 會話 2 具有在子級上授予的可升級(寫入)鎖定,並且正在等待父級的讀取鎖定。最後會話 3 具有針對父級的授權寫入鎖定:

mysql> select * from performance_schema.metadata_locks;
+-------------+-------------+-------------------+---------------+-------------+
| OBJECT_TYPE | OBJECT_NAME | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS |
+-------------+-------------+-------------------+---------------+-------------+
| TABLE | child | SHARED_UPGRADABLE | TRANSACTION | GRANTED | <- ALTER (S2)
| TABLE | parent | SHARED_WRITE | TRANSACTION | GRANTED | <- UPDATE (S1)
| TABLE | parent | SHARED_WRITE | TRANSACTION | GRANTED | <- UPDATE (S3)
| TABLE | parent | SHARED_READ_ONLY | STATEMENT | PENDING | <- ALTER (S2)
+-------------+-------------+-------------------+---------------+-------------+

請注意,具有掛起鎖定狀態的唯一會話是會話 2(ALTER)。會話 1 和會話 3 (分別在 ALTER 之前和之後釋出)都被授予了寫鎖。排序失敗的地方是在會話 1 上發生提交的時候。在考慮有序佇列時,人們會期望會話 2 獲得鎖定,事情就會繼續進行。但是,由於元資料鎖定系統的優先順序性質,會話 3 具有鎖定,會話 2 仍然等待。

如果另一個寫入會話進入並啟動新事務並獲取針對父表的寫鎖定,則即使會話 3 完成,ALTER 仍將被阻止。

只要我保持一個對父表開啟元資料鎖定的活動事務,子表上的 ALTER 將永遠不會完成。更糟糕的是,由於子表上的寫鎖定成功(但是完整語句正在等待獲取父讀鎖定),所以針對子表的所有傳入讀取請求都將被阻止!

另外,請考慮一下您通常如何對無法完成的語句進行故障排除。您檢視已經開啟較長時間的事務(在程序列表和 InnoDB 狀態中)。但由於阻塞執行緒現在比 ALTER 執行緒更年輕,因此您將看到的最舊的事務/執行緒是 ALTER 。

這正是這種情況下發生的情況。在準備釋出時,我們的客戶端正在執行 ALTER 語句並結合負載測試(一種非常好的做法!)以確保順利釋出。問題是負載測試保持對父表開啟一個活動的寫事務。這並不是說它只是一直在寫,而是有多個執行緒,一個總是活躍的。 這阻止了 ALTER 完成並阻止對相對靜態的子表的隨後的讀請求。

幸運的是,這個問題有一個解決方案(除了從設計模式中驅逐外來鍵)。變數** max_write_lock_count**[2]可用於允許在寫入鎖定之後在讀取鎖定之前授予讀取鎖定連續寫鎖。預設情況下,此變數設定為 18446744073709551615,如果你對該表發出 10,000 次寫入/秒,那麼你的讀將被鎖定 5800 萬年……

[2]:https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_write_lock_count

為了防止這種海枯石爛的情況發生,您可以簡單地將 max_write_lock_count 減少到一個較小的數字(例如10?),並且在獲取每 10 個寫鎖定之後,元資料鎖定系統的子系統將查詢掛起的讀鎖定並授予 1 個,然後返回寫入。問題解決!

作為動態變數,可以在執行時調整它以允許等待 ALTER 完成。一般來說,這更像是一種邊緣情況,因為在寫入表之間通常會有一些時間來獲取讀鎖定。但是,如果您的用例使併發會話保持執行狀態,那麼 常常會對作為外來鍵引用的表進行事務處理,您可能會看到這種情況。幸運的是,修復很簡單,可以動態完成!

注意:通過效能模式和啟用 metadata_locks 表可以進行此故障排除。

如下所述:

https://dev.mysql.com/doc/refman/5.7/en/metadata-locks-table.ht