1. 程式人生 > 資料庫 >Mysql全域性鎖和表鎖(五)

Mysql全域性鎖和表鎖(五)

根據加鎖的範圍,MySQL裡面的鎖大致可以分成全域性鎖、表級鎖和行鎖三類。

全域性鎖

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

全域性鎖的典型使用場景是,做全庫邏輯備份。也就是把整庫每個表都select出來存成文字。
注意,在備份過程中整個庫完全處於只讀狀態,這會讓業務系統停擺,所以是很危險的操作。

加全域性鎖有風險。但備份為什麼要加鎖呢?

假設你現在要維護“極客時間”的購買系統,關注的是使用者賬戶餘額表和使用者課程表。現在發起一個邏輯備份。假裝置份期間,有一個使用者,他購買了一門課程,業務邏輯裡就要扣掉他的餘額,然後往已購課程裡面加上一門課。如果時間順序上是先備份賬戶餘額表(u_account),然後使用者購買,然後備份使用者課程表(u_course),會怎麼樣呢?你可以看一下這個圖:

在這裡插入圖片描述

這個備份結果裡,使用者A的資料狀態是“賬戶餘額沒扣,但是使用者課程表裡面已經多了一門課”。如果後面用這個備份來恢復資料的話,使用者A就發現,自己賺了。作為使用者可別覺得這樣可真好啊,你可以試想一下:如果備份表的順序反過來,先備份使用者課程表,再備份賬戶餘額表,發生的就是另一個不好的場景了。

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

有了這個功能,為什麼還需要FTWRL呢?一致性讀是好,但前提是引擎要支援這個隔離級別。比如,對於MyISAM這種不支援事務的引擎,如果備份過程中有更新,總是隻能取到最新的資料,那麼就破壞了備份的一致性。

這時,我們就需要使用FTWRL命令了。所以,single-transaction 方法只適用於所有的表使用事務引擎的庫。 如果有的表使用了不支援事務的引擎,那麼備份就只能通過FTWRL方法。

既然要全庫只讀,為什麼不使用 set global readonly=true 的方式呢
建議用FTWRL方式,主要有兩個原因:

  • 在有些系統中,readonly 的值會被用來做其他邏輯,比如用來判斷一個庫是主庫還是備庫。因此,修改global變數的方式影響面更大,不建議使用。
  • 在異常處理機制上有差異。如果執行FTWRL命令之後由於客戶端發生異常斷開,那麼MySQL會自動釋放這個全域性鎖,整個庫回到可以正常更新的狀態。而將整個庫設定為readonly之後,如果客戶端發生異常,則資料庫就會一直保持readonly狀態,這樣會導致整個庫長時間處於不可寫狀態,風險較高。

表級鎖

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(metadata lock)。MDL不需要顯式使用,在訪問一個表的時候會被自動加上。MDL的作用是,保證讀寫的正確性。
如果一個查詢正在遍歷一個表中的資料,而執行期間另一個執行緒對這個表結構做變更,刪了一列,那麼查詢執行緒拿到的結果跟表結構對不上,肯定是不行的。

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

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

雖然MDL鎖是系統預設會加的,但卻是不能忽略的一個機制。
比如下面這個例子,經常有人掉到這個坑裡:給一個小表加個欄位,導致整個庫掛了。
你肯定知道,給一個表加欄位,或者修改欄位,或者加索引,需要掃描全表的資料。在對大表操作的時候,你肯定會特別小心,以免對線上服務造成影響。而實際上,即使是小表,操作不慎也會出問題。我們來看一下下面的操作序列,假設表t是一個小表。

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

  • sessionA 先啟動,會對錶加一個MDL讀鎖,sessionB啟動後也會加一個MDL讀鎖,讀鎖之間不互斥,因此可以正常執行;
  • 之後sessionC會被blocked,因為sessionA的MDL讀鎖還沒釋放,而sessionC需要MDL寫鎖,讀寫鎖之間是互斥的,因此只能被阻塞。
  • 然後sessionD也需要一個MDL讀鎖,會被sessionC阻塞,也就是說這張表現在完全變成不可讀寫了。

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

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

注: DDL語句如修改表結構,增加列等操作是隱式提交的

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

首先我們要解決長事務,事務不提交,就會一直佔著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引數,對應用會更友好。
表鎖一般是在資料庫引擎不支援行鎖的時候才會被用到的。如果你發現你的應用程式裡有locktables這樣的語句,你需要追查一下,比較可能的情況是:要麼是你的系統現在還在用MyISAM這類不支援事務的引擎,那要安排升級換引擎;要麼是你的引擎升級了,但是程式碼還沒升級。我見過這樣的情況,最後業務開發就是把locktables 和 unlock tables 改成 begin 和 commit,問題就解決了。MDL會直到事務提交才釋放,在做表結構變更的時候,你一定要小心不要導致鎖住線上查詢和更新。