MySQL深入學習第六篇 - 全局鎖和表鎖 :給表加個字段怎麼有這麼多阻礙?

數據庫鎖設計的初衷是處理併發問題。作爲多用戶共享的資源,當出現併發訪問的時候,數據庫需要合理地控制資源的訪問規則。而鎖就是用來實現這些訪問規則的重要數據結構。

根據加鎖的範圍,MySQL 裏面的鎖大致可以分成全局鎖、表級鎖和行鎖三類。今天這篇文章,將會分享全局鎖和表級鎖。而關於行鎖的內容,會在下一篇文章中詳細介紹。

這裏需要說明的是,鎖的設計比較複雜,這兩篇文章不會涉及鎖的具體實現細節,主要介紹的是碰到鎖時的現象和其背後的原理。

 

全局鎖

顧名思義,全局鎖就是對整個數據庫實例加鎖。MySQL 提供了一個加全局讀鎖的方法,命令是 Flush tables with read lock (FTWRL)。當你需要讓整個庫處於只讀狀態的時候,可以使用這個命令,之後其他線程的以下語句會被阻塞:數據更新語句(數據的增刪改)、數據定義語句(包括建表、修改表結構等)和更新類事務的提交語句。

全局鎖的典型使用場景是,做全庫邏輯備份。也就是把整庫每個表都 select 出來存成文本。

以前有一種做法,是通過 FTWRL 確保不會有其他線程對數據庫做更新,然後對整個庫做備份。注意,在備份過程中整個庫完全處於只讀狀態。

但是讓整庫都只讀,聽上去就很危險:

1. 如果你在主庫上備份,那麼在備份期間都不能執行更新,業務基本上就得停擺;

2. 如果你在從庫上備份,那麼備份期間從庫不能執行主庫同步過來的 binlog,會導致主從延遲。

看來加全局鎖不太好。但是細想一下,備份爲什麼要加鎖呢?我們來看一下不加鎖會有什麼問題。

假設你現在要維護在線學習的購買系統,關注的是用戶賬戶餘額表和用戶課程表。

現在發起一個邏輯備份。假設備份期間,有一個用戶,他購買了一門課程,業務邏輯裏就要扣掉他的餘額,然後往已購課程裏面加上一門課。

如果時間順序上是先備份賬戶餘額表 (u_account),然後用戶購買,然後備份用戶課程表 (u_course),會怎麼樣呢?你可以看一下這個圖:

可以看到,這個備份結果裏,用戶 A 的數據狀態是“賬戶餘額沒扣,但是用戶課程表裏面已經多了一門課”。如果後面用這個備份來恢復數據的話,用戶 A 就發現,自己賺了。

作爲用戶可別覺得這樣可真好啊,你可以試想一下:如果備份表的順序反過來,先備份用戶課程表再備份賬戶餘額表,又可能會出現什麼結果?

也就是說,不加鎖的話,備份系統備份的得到的庫不是一個邏輯時間點,這個視圖是邏輯不一致的。

說到視圖你肯定想起來了,我們在前面講事務隔離的時候,其實是有一個方法能夠拿到一致性視圖的,對吧?

MySQL深入學習第三篇 -事務隔離:爲什麼你改了我還看不見?

是的,就是在可重複讀隔離級別下開啓一個事務。

官方自帶的邏輯備份工具是 mysqldump。當 mysqldump 使用參數–single-transaction 的時候,導數據之前就會啓動一個事務,來確保拿到一致性視圖。而由於 MVCC 的支持,這個過程中數據是可以正常更新的。

你一定在疑惑,有了這個功能,爲什麼還需要 FTWRL 呢?一致性讀是好,但前提是引擎要支持這個隔離級別。比如,對於 MyISAM 這種不支持事務的引擎,如果備份過程中有更新,總是隻能取到最新的數據,那麼就破壞了備份的一致性。這時,我們就需要使用 FTWRL 命令了。

所以,single-transaction 方法只適用於所有的表使用事務引擎的庫。如果有的表使用了不支持事務的引擎,那麼備份就只能通過 FTWRL 方法。這往往是 DBA 要求業務開發人員使用 InnoDB 替代 MyISAM 的原因之一。

你也許會問,既然要全庫只讀,爲什麼不使用 set global readonly=true 的方式呢?確實 readonly 方式也可以讓全庫進入只讀狀態,但我還是會建議你用 FTWRL 方式,主要有兩個原因:

一是,在有些系統中,readonly 的值會被用來做其他邏輯,比如用來判斷一個庫是主庫還是備庫。因此,修改 global 變量的方式影響面更大,我不建議你使用。

二是,在異常處理機制上有差異。如果執行 FTWRL 命令之後由於客戶端發生異常斷開,那麼 MySQL 會自動釋放這個全局鎖,整個庫回到可以正常更新的狀態。而將整個庫設置爲 readonly 之後,如果客戶端發生異常,則數據庫就會一直保持 readonly 狀態,這樣會導致整個庫長時間處於不可寫狀態,風險較高。

業務的更新不只是增刪改數據(DML),還有可能是加字段等修改表結構的操作(DDL)。不論是哪種方法,一個庫被全局鎖上以後,你要對裏面任何一個表做加字段操作,都是會被鎖住的。

但是,即使沒有被全局鎖住,加字段也不是就能一帆風順的,因爲你還會碰到接下來我們要介紹的表級鎖。

 

表級鎖

MySQL 裏面表級別的鎖有兩種:一種是表鎖,一種是元數據鎖(meta data lock,MDL)。

表鎖的語法是 lock tables … read/write。與 FTWRL 類似,可以用 unlock tables 主動釋放鎖,也可以在客戶端斷開的時候自動釋放。需要注意,lock tables 語法除了會限制別的線程的讀寫外,也限定了本線程接下來的操作對象。

舉個例子, 如果在某個線程 A 中執行 lock tables t1 read, t2 write; 這個語句,則其他線程寫 t1、讀寫 t2 的語句都會被阻塞。同時,線程 A 在執行 unlock tables 之前,也只能執行讀 t1、讀寫 t2 的操作。連寫 t1 都不允許,自然也不能訪問其他表。

在還沒有出現更細粒度的鎖的時候,表鎖是最常用的處理併發的方式。而對於 InnoDB 這種支持行鎖的引擎,一般不使用 lock tables 命令來控制併發,畢竟鎖住整個表的影響面還是太大。

另一類表級的鎖是 MDL(meta data lock)。MDL 不需要顯式使用,在訪問一個表的時候會被自動加上。MDL 的作用是,保證讀寫的正確性。你可以想象一下,如果一個查詢正在遍歷一個表中的數據,而執行期間另一個線程對這個表結構做變更,刪了一列,那麼查詢線程拿到的結果跟表結構對不上,肯定是不行的。

因此,在 MySQL 5.5 版本中引入了 MDL,當對一個表做增刪改查操作的時候,加 MDL 讀鎖;當要對錶做結構變更操作的時候,加 MDL 寫鎖。

1. 讀鎖之間不互斥,因此你可以有多個線程同時對一張表增刪改查。

2. 讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結構操作的安全性。因此,如果有兩個線程要同時給一個表加字段,其中一個要等另一個執行完才能開始執行。

雖然 MDL 鎖是系統默認會加的,但卻是你不能忽略的一個機制。比如下面這個例子,我經常看到有人掉到這個坑裏:給一個小表加個字段,導致整個庫掛了。

你肯定知道,給一個表加字段,或者修改字段,或者加索引,需要掃描全表的數據。在對大表操作的時候,你肯定會特別小心,以免對線上服務造成影響。而實際上,即使是小表,操作不慎也會出問題。我們來看一下下面的操作序列,假設表 t 是一個小表。

備註:這裏的實驗環境是 MySQL 5.6

我們可以看到 session A 先啓動,這時候會對錶 t 加一個 MDL 讀鎖。由於 session B 需要的也是 MDL 讀鎖,因此可以正常執行。

之後 session C 會被 blocked,是因爲 session A 的 MDL 讀鎖還沒有釋放,而 session C 需要 MDL 寫鎖,因此只能被阻塞。

如果只有 session C 自己被阻塞還沒什麼關係,但是之後所有要在表 t 上新申請 MDL 讀鎖的請求也會被 session C 阻塞。前面我們說了,所有對錶的增刪改查操作都需要先申請 MDL 讀鎖,就都被鎖住,等於這個表現在完全不可讀寫了。

如果某個表上的查詢語句頻繁,而且客戶端有重試機制,也就是說超時後會再起一個新 session 再請求的話,這個庫的線程很快就會爆滿。

你現在應該知道了,事務中的 MDL 鎖,在語句執行開始時申請,但是語句結束後並不會馬上釋放,而會等到整個事務提交後再釋放。

基於上面的分析,我們來討論一個問題,如何安全地給小表加字段?

首先我們要解決長事務,事務不提交,就會一直佔着 MDL 鎖。在 MySQL 的 information_schema 庫的 innodb_trx 表中,你可以查到當前執行中的事務。如果你要做 DDL 變更的表剛好有長事務在執行,要考慮先暫停 DDL,或者 kill 掉這個長事務。

但考慮一下這個場景。如果你要變更的表是一個熱點表,雖然數據量不大,但是上面的請求很頻繁,而你不得不加個字段,你該怎麼做呢?

這時候 kill 可能未必管用,因爲新的請求馬上就來了。比較理想的機制是,在 alter table 語句裏面設定等待時間,如果在這個指定的等待時間裏面能夠拿到 MDL 寫鎖最好,拿不到也不要阻塞後面的業務語句,先放棄。之後開發人員或者 DBA 再通過重試命令重複這個過程。

MariaDB 已經合併了 AliSQL 的這個功能,所以這兩個開源分支目前都支持 DDL NOWAIT/WAIT n 這個語法。

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ... 

 

小結

今天,我跟你介紹了 MySQL 的全局鎖和表級鎖。

全局鎖主要用在邏輯備份過程中。對於全部是 InnoDB 引擎的庫,我建議你選擇使用–single-transaction 參數,對應用會更友好。

表鎖一般是在數據庫引擎不支持行鎖的時候纔會被用到的。如果你發現你的應用程序裏有 lock tables 這樣的語句,你需要追查一下,比較可能的情況是:

1. 要麼是你的系統現在還在用 MyISAM 這類不支持事務的引擎,那要安排升級換引擎;

2. 要麼是你的引擎升級了,但是代碼還沒升級。我見過這樣的情況,最後業務開發就是把 lock tables 和 unlock tables 改成 begin 和 commit,問題就解決了。

MDL 會直到事務提交才釋放,在做表結構變更的時候,你一定要小心不要導致鎖住線上查詢和更新。

最後,我給你留一個問題吧。

備份一般都會在備庫上執行,你在用–single-transaction 方法做邏輯備份的過程中,如果主庫上的一個小表做了一個 DDL,比如給一個表上加了一列。這時候,從備庫上會看到什麼現象呢?

問題解答如下:

假設這個 DDL 是針對表 t1 的, 這裏我把備份過程中幾個關鍵的語句列出來:

Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

Q2:START TRANSACTION  WITH CONSISTENT SNAPSHOT;

/* other tables */
Q3:SAVEPOINT sp;

/* 時刻 1 */
Q4:show create table `t1`;

/* 時刻 2 */
Q5:SELECT * FROM `t1`;

/* 時刻 3 */
Q6:ROLLBACK TO SAVEPOINT sp;

/* 時刻 4 */
/* other tables */

在備份開始的時候,爲了確保 RR(可重複讀)隔離級別,再設置一次 RR 隔離級別 (Q1);

啓動事務,這裏用 WITH CONSISTENT SNAPSHOT 確保這個語句執行完就可以得到一個一致性視圖(Q2);

設置一個保存點,這個很重要(Q3);

show create 是爲了拿到表結構 (Q4),

然後正式導數據 (Q5),

回滾到 SAVEPOINT sp,在這裏的作用是釋放 t1 的 MDL 鎖 (Q6)。

DDL 從主庫傳過來的時間按照效果不同,我打了四個時刻。題目設定爲小表,我們假定到達後,如果開始執行,則很快能夠執行完成。

參考答案如下:

1. 如果在 Q4 語句執行之前到達,現象:沒有影響,備份拿到的是 DDL 後的表結構。

2. 如果在“時刻 2”到達,則表結構被改過,Q5 執行的時候,報 Table definition has changed, please retry transaction,現象:mysqldump 終止;

3. 如果在“時刻 2”和“時刻 3”之間到達,mysqldump 佔着 t1 的 MDL 讀鎖,binlog 被阻塞,現象:主從延遲,直到 Q6 執行完成。

4. 從“時刻 4”開始,mysqldump 釋放了 MDL 讀鎖,現象:沒有影響,備份拿到的是 DDL 前的表結構。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章