1. 程式人生 > >【搞定MySQL資料庫】第7篇:MySQL中的鎖:全域性鎖、表鎖、行鎖

【搞定MySQL資料庫】第7篇:MySQL中的鎖:全域性鎖、表鎖、行鎖

本文為本人學習極客時間《MySQL實戰45講》的學習筆記。

原文連結:https://time.geekbang.org/column/article/69862

                  https://time.geekbang.org/column/article/70215

本文目錄:

1、全域性鎖

2、表級鎖

3、行鎖

3.1、兩階段鎖協議

3.2、死鎖和死鎖的檢測

4、總結


本文用來聊聊 MySQL 的鎖。資料庫鎖設計的初衷是處理併發問題。作為多使用者共享的資源,當出現併發訪問的時候,資料庫需要合理地控制資源的訪問規則。而鎖就是用來實現這些訪問規則的重要資料結構。

根據加鎖的範圍,MySQL 裡面的鎖大致可以分成全域性鎖表級鎖行鎖三類。鎖的設計比較複雜,本文介紹的主要是碰到鎖時的現象和其背後的原理。

1、全域性鎖

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

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

業務和備份狀態圖

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

前面講事務隔離的時候,在可重複讀隔離級別下開啟一個事務是可以拿到一致性檢視的。

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

你也許還會問,既然要全庫只讀,為什麼不使用 set  global  readonly = true 的方式呢?

readonly 方式確實是可以讓全庫進入只讀狀態,但是這裡還是建議使用 FTWRL ,原因如下:

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

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


2、表級鎖

MySQL 裡面表級別的鎖有兩種:一種是表鎖,一種是元資料鎖(meta  data  lock, MDL)。

  • 表鎖

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

  • 元資料鎖:MDL

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

因此,在 MySQL5.5 版本中引入 MDL,當對一個表做增刪該查操作時,加 MDL 讀鎖;當對錶結構變更操作的時候,加 MDL 寫鎖。

  • 讀鎖之間不互斥,因此你可以有多個執行緒同時對一張表增刪該查;
  • 讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結構操作的安全性。因此,如果有兩個執行緒要同時給一個表加欄位,其中一個要等另一個執行完後才能開始執行。

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

基於上面的討論,我們考慮下:如何安全地給小表加欄位?

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

3、行鎖

MySQL 的行鎖是在引擎層由各個引擎自己實現的。但並不是所有的引擎都支援行鎖,比如 MyISAM 引擎就不支援行鎖。不支援行鎖意味著併發控制使用表鎖,對於這種引擎的表,同一張表上任何時刻只能有一個更新在執行。這就會影響到業務的併發度。InnoDB 是支援行鎖的,這也是 MyISAM 被 InnoDB 替代的重要原因之一。

下面就聊聊 InnoDB 的行鎖,以及如何通過減少鎖衝突來提升業務併發度的。

行鎖就是針對資料表中行記錄的鎖。比如:事務 A 更新了一行,而這時候事務 B 也要更新同一行,則必須等事務 A 的操作完成之後才能進行更新。

3.1、兩階段鎖協議

先看個例子,在下面的操作序列中,事務B 的 update 語句執行時會是什麼現象呢?假設欄位 id 是表 t 的主鍵。

這個問題的結論取決於事務 A 在執行完兩條 update 語句後,持有哪些鎖,以及在什麼時候釋放。你可以驗證下:實際上事務 B 的 update 語句會被阻塞,直到事務 A 執行 commit 之後,事務 B 才能繼續執行。

所以,上圖中事務 A 持有的兩個記錄的行鎖,都是在 commit 的時候才釋放掉的。

也就是說,在 InnoDB 事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。

知道了這個設定,如果你的事務中需要鎖多個行,要把最可能造成鎖衝突、最可能影響併發度的鎖儘量往後放。(這樣佔用的時間就少,沒佔用的時間其他執行緒還可以用)。下面舉個例子。

3.2、死鎖和死鎖的檢測

當發現系統中不同執行緒出現迴圈資源依賴,涉及的執行緒都在等待別的執行緒釋放資源時,就會導致這幾個執行緒都進入無限等待的狀態,稱為死鎖。

下面用資料庫的行鎖舉個死鎖的例子:

這個時候,事務 A 在等待事務 B 釋放 id=2 的行鎖,而事務 B 在等待事務 A 釋放 id = 1 的行鎖。事務 A 和事務 B 在互相等待對方的資源釋放,就是進入了死鎖狀態。當出現死鎖以後,有兩種策略:

1、一種策略是:直接進入等待,直到超時。這個超時時間可以通過引數:innodb_lock_wait_timeout 來設定;

2、另一種策略是:發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。將引數 innodb_deadlock_detect 設定為 on,表示開啟這個邏輯。

根據前面的分析,我們來討論下:如何解決這種熱點行更新導致的效能問題呢?問題的癥結在於:死鎖檢測要耗費大量的 CPU 資源。

一種頭痛醫頭的方法是:如果你能確保這個業務一定不會出現死鎖,可以臨時把死鎖檢測給關掉。但是這種操作本身帶有一定的風險,因為業務設計的時候一般不會把死鎖當做一個嚴重的錯誤,畢竟出現死鎖了,就回滾,然後通過業務重試一般就沒有問題了,這是業務無損的。而關掉死鎖檢測意味著可能會出現大量的超時,這是業務有損的。

另外一種思路就是控制併發度:根據上面的分析,你會發現如果併發度能夠控制。比如,同一行同時最多隻有 10 個執行緒在更新,那麼死鎖檢測的成本很低,就不會出現這個問題。


4、總結

1、全域性鎖主要用在邏輯備份的過程中。對於全部是 InnoDB 引擎的庫,建議使用 -single  transaction 引數,對應用更加友好;

2、表鎖一般是在資料庫引擎不支援行鎖的時候才會用到;

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

4、兩階段協議:如何正確安排事務的語句,提高併發度:如果你的事務中需要多個行,要把最可能造成鎖衝突或者最可能影響併發度的鎖的申請時間放到最後;

5、調整語句順序並不能完全避免死鎖,所以引入了死鎖和死鎖檢測的概念,以及提供了三個方案來減少死鎖對資料庫的影響。減少死鎖的主要方向是:控制訪問相同資源的併發事務量。