高性能MySQL之鎖詳解

1、背景

MySQL裏面的鎖大體能夠分紅全局鎖、表級鎖和行鎖三類。數據庫鎖的設計的初衷是處理併發問題。咱們知道多用戶共享資源的時候,就有可能會出現併發訪問的時候,數據庫就須要合理的控制資源的訪問規則,所以,鎖就應運而生了,它主要用來實現這些訪問規則的重要數據結構。mysql

 

2、全局鎖

顧名思義,全局鎖就是對整個數據庫實例加鎖,能夠經過命令 Flush tables with read lock (FTWRL)對整個數據庫實例子加鎖。讓整個庫處於只讀狀態的時候,可使用這個命令,以後其餘線程的如下語句會被阻塞:數據更新語句(數據的增刪改)、數據定義語句(包括建表、修改表結構等)和更新類事務的提交語句。sql

全局鎖有一個經典的使用場景就是作全庫邏輯備份,也就是說吧整個數據庫的每一個表都用select 出來存成文本。之前有一種作法是經過FTWRL確保不會有其餘線程對數據庫作更新,而後對整個庫作備份。注意,在備份過程當中整個庫徹底處於只讀狀態。數據庫

你此時是否是以爲很危險?安全

若是你在主庫上備份,那麼在備份期間都不能執行更新,業務基本上就得停擺;數據結構

若是你在從庫上備份,那麼備份期間從庫不能執行主庫同步過來的binlog,會致使主從延遲。併發

看上去確實很危險,可是咱們細想一下,備份爲何要加鎖呢?若是咱們不加鎖又會出現什麼問題呢?工具

假設你如今要維護京東的購買系統,關注的是用戶帳戶餘額表和用戶商品表。性能

如今發起一個邏輯備份。假設備份期間,有一個用戶,他購買了一件商品,業務邏輯裏就要扣掉他的餘額,而後往已購商品裏面加上一件商品。優化

若是時間順序上是先備份帳戶餘額表(u_account),而後用戶購買,而後備份用戶商品表(u_course),會怎麼樣呢?你能夠看一下這個圖:spa

從上圖能夠看到用戶的數據狀態是「帳戶餘額沒扣,可是用戶商品裏面已經多了一件商品」。若是後面用這個備份來恢復數據的話,用戶竟然發現本身本身帳戶竟然無故端的多了50塊,站在公司角度,你收拾包袱走人吧。可是用戶也別高興,若是備份表的順序反過來,先備份用戶商品表再備份帳戶餘額表,又可能會出現什麼結果呢?

那固然是後面用這個備份來恢復數據的話,用戶竟然發現本身帳戶的錢被扣了,可是卻沒有買到剃鬚刀。也就是說,不加鎖的話,備份系統備份的獲得的庫不是一個邏輯時間點,這個視圖是邏輯不一致的。這是時候你確定想道我前面文章所講的講事務隔離,實際上是有一個方法可以拿到一致性視圖的。

是的,毫無疑問 就是在可重複讀隔離界別下開啓一個事務可以拿到一致性視圖。

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

既然有那麼好用的功能,爲何還須要FTWRL去作備份呢?

一致性讀是好,可是不要忘記一點,前提就是引擎要支持這個隔離級別。好比,對於MyISAM這種不支持事務的引擎,若是備份過程當中有更新,老是隻能取到最新的數據,那麼就破壞了備份的一致性。這時候FTWRL命令了就派上用場了。

所以,single-transaction方法只能夠用於全部的表使用事務引擎的庫。若是有的表使用了不支持事務的引擎,那麼備份就只能經過FTWRL方法。這就是爲何InnoDB比myISAM普及的緣由之一。

到這裏,你也許會會想到,爲何咱們不用set global readonly=true 的命令讓全庫處於只讀的狀態呢?

注意,這是生產上嚴厲禁止的,主要有以下兩個緣由:

  1.在某些系統中,readonly的值會被用來作其餘邏輯,好比用來判斷一個庫是主庫仍是備庫。毫無疑問修改global變量的方式影響面更大。

  2.在異常處理機制上存在差別。若是執行FTWRL命令以後因爲客戶端發生異常斷開,那麼MySQL會自動釋放這個全局鎖,整個庫回到能夠正常更新的狀態。而將整個庫設置爲readonly以後,若是客戶端發生異常,則數據庫就會一直保持readonly狀態,這樣會致使整個庫長時間處於不可寫狀態,很容易形成生產事故。

 

3、表級別的鎖

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

表鎖的語法是 lock tables … read/write。與FTWRL相似,能夠用unlock tables主動釋放鎖,也能夠在客戶端斷開的時候自動釋放。須要注意,lock tables語法除了會限制別的線程的讀寫外,也限定了本線程接下來的操做對象。舉個例子, 若是在某個線程A中執行lock tables t1 read, t2 write; 這個語句,則其餘線程寫t一、讀寫t2的語句都會被阻塞。同時,線程A在執行unlock tables以前,也只能執行讀t一、讀寫t2的操做。連寫t1都不容許,天然也不能訪問其餘表。

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

另外一類表級的鎖是MDL(metadata lock)。MDL不須要顯式使用,在訪問一個表的時候會被自動加上。當對一個表作增刪改查操做的時候,加MDL讀鎖;當要對錶作結構變動操做的時候,加MDL寫鎖。你能夠想象一下,若是一個查詢正在遍歷一個表中的數據,而執行期間另外一個線程對這個表結構作變動,刪了一列,那麼查詢線程拿到的結果跟表結構對不上,確定是不行的。所以,在MySQL 5.5版本中引入了MDL。

咱們知道讀鎖之間不互斥,所以你能夠有多個線程同時對一張表增刪改查。可是讀寫鎖之間、寫鎖之間是互斥的,用來保證變動表結構操做的安全性。所以,若是有兩個線程要同時給一個表加字段,其中一個要等另外一個執行完才能開始執行。

請注意,不少人在MDL鎖稍微不注意就會掉入這個坑裏:給一個小表加個字段,最後致使整個庫掛了。咱們都知道給一個表加字段,或者修改字段,或者加索引,須要掃描全表的數據。在對大表操做的時候,你確定會特別當心,以避免對線上服務形成影響。而實際上,即便是小表,操做不慎也會出問題。咱們來看一下下面的操做序列,假設表t是一個小表。

如上圖所示,會話A 先啓動,這時候會對錶t 加 MDL讀鎖。會話 B也是MDL讀鎖,咱們知道,經過上面的知識知道,MDL讀鎖以前是不互斥的,所以能夠正常執行。

接着會話C會被阻塞,爲何會被阻塞呢?結合上面的知識,咱們知道會話 A的MDL讀鎖尚未釋放,而會話 C須要MDL寫鎖,所以只能被阻塞。若是隻有會話 C本身被阻塞也就還好,細想一下以後全部要在表t上新申請MDL讀鎖的請求也會被會話 C阻塞。經過前面的知識,咱們知道全部對錶的增刪改查操做都須要先申請MDL讀鎖,就都被鎖住,等於這個表如今徹底不可讀寫了。若是某個表上存在頻繁的語句查詢,並且客戶端有重試這個機制在,超時後會再起一個新會話再請求的話,這個庫的線程很快就會爆滿。這就是爲何即便是小表,操做不慎,最後致使整個庫掛了。咱們如今知道了事務中的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 ...

 

接下來聊聊InnoDB的行鎖,以及如何經過減小鎖衝突來提高業務併發度。爲何我不講解基於MyISAM的呢?,你們別忘了咱們前面提到的MyISAM引擎就不支持行鎖。不支持行鎖意味着併發控制只能使用表鎖,對於這種引擎的表,同一張表上任什麼時候刻只能有一個更新在執行,這就會影響到業務併發度。InnoDB是支持行鎖的,這也是MyISAM被InnoDB替代的重要緣由之一。

 

那什麼是行鎖呢?見其名知其意,行鎖主要是針對數據庫表中行記錄的鎖,舉個通熟易懂的例子,好比事務A更新一行,與此同時,事務B 也要要更新同一行,則必須等事務A的操做完成後才能進行更新。

我這裏爲何要講這些概念性東西呢?很簡單,若是咱們對概念的理解不透徹,進行生產的時候,一不當心就致使程序出現一些非預期的行爲。就比如如二階段鎖。

接下來經過一個例子講解二階段鎖的注意事項,例子以下:

從上圖能夠看到,按照時間的順序操做,事務執行update 語句時,會發什麼事情呢?上圖的 id 是表T的主鍵。

這問題主要看事務A在執行完兩條update 後,擁有哪些鎖,在何時釋放鎖。很明顯事務B 的update 會被阻塞,知道事務A執行commit提交時候後,事務B才能繼續執行。由於事務A持有的兩個記錄的行鎖,都是commit 的時候才釋放的。

所以,在InnoDB事務中,行鎖是在須要的時候才加上的,但並非不須要了就馬上釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。

咱們知道這個設定,有什麼用呢?貌似對於咱們使用事務有什麼幫助呢?

仍是頗有幫助的,例如,若是你的事務中須要鎖多個行,要把最可能形成鎖衝突、最可能影響併發度的鎖儘可能日後放。若是你此時負責實現一個在線交易的購物平臺,用戶A在某東上購買了一部手機,這個過程主要涉及一下幾個操做:

  1.從用戶A帳戶中扣除手機的價錢;

  2.給某東的帳戶餘額增長這部手機的價錢;

  3.記錄一條交易日誌。

這個操做過程,爲了保證交易的原子性,必然要把這三個操做放在一個事務中的,咱們須要update 兩條記錄,而且insert 一條記錄,那麼咱們如何安排這三個語句在事務中的順序呢?

若是此時還有另一個用戶B在某東上買了一本Java,那麼兩個事務中衝突的部門必然是語句 2 了,由於它們要更新某東帳戶的餘額,須要更改同一行數據。

根據兩階段鎖協議,不論你怎樣安排語句順序,全部的操做須要的行鎖都是在事務提交的時候才釋放的。因此,若是你把語句2安排在最後,好比按照三、一、2這樣的順序,必然某東餘額這一行的鎖時間就最少。這就最大程度地減小了事務之間的鎖等待,提高了併發度。

雖然餘額這一行的行鎖在一個事務中不會停留很長時間,可是並不能徹底解決問題。

下面再舉個例子,若是某東 6.18活動,低價預售全部的商品,活動剛開始的時候,你發現你的數據庫忽然就掛了,那麼此時你進行排查問題,top 命令等一系列操做,因而看到CPU 幾乎百分百,可是整個數據庫每秒就執行不到2000個事務(這裏我只是假設的呀,我也不知道某東的具體狀況,不要擡槓,哈哈)。到這裏,就必須說說死鎖和死鎖的檢測了。

 

4、死鎖和死鎖檢測

當併發系統中不一樣線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會致使這幾個線程都進入無限等待的狀態,稱爲死鎖。接下來用行鎖舉個例子。

從上圖能夠看到,事務A在等待事務B釋放id=2的行鎖,而事務B在等待事務A釋放id=1的行鎖。 事務A和事務B在互相等待對方的資源釋放,就是進入了死鎖狀態。

當出現死鎖之後,有兩種策略:

  • 一種策略是,直接進入等待,直到超時。這個超時時間能夠經過參數innodb_lock_wait_timeout來設置。在InnoDB中,innodb_lock_wait_timeout的默認值是50s,意味着若是採用第一個策略,當出現死鎖之後,第一個被鎖住的線程要過50s纔會超時退出,而後其餘線程纔有可能繼續執行。對於在線服務來講,這個等待時間每每是沒法接受的。可是,咱們又不可能直接把這個時間設置成一個很小的值,好比1s。這樣當出現死鎖的時候,確實很快就能夠解開,但若是不是死鎖,而是簡單的鎖等待呢?因此,超時時間設置過短的話,會出現不少誤傷。
  • 另外一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其餘事務得以繼續執行。將參數innodb_deadlock_detect設置爲on,表示開啓這個邏輯。因此,正常狀況下咱們仍是要採用第二種策略,即:主動死鎖檢測,並且innodb_deadlock_detect的默認值自己就是on。主動死鎖檢測在發生死鎖的時候,是可以快速發現並進行處理的,可是它也是有額外負擔的。

你能夠想象一下這個過程:每當一個事務被鎖的時候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環,最後判斷是否出現了循環等待,也就是死鎖。

那若是是咱們上面說到的全部事務都要更新同一行的場景呢?

每一個新來的被堵住的線程,都要判斷會不會因爲本身的加入致使了死鎖,這是一個時間複雜度是O(n)的操做。假設有1000個併發線程要同時更新同一行,那麼死鎖檢測操做就是100萬這個量級的。雖然最終檢測的結果是沒有死鎖,可是這期間要消耗大量的CPU資源。所以,你就會看到CPU利用率很高,可是每秒卻執行不了幾個事務。

根據上面的分析,咱們來討論一下,怎麼解決由這種熱點行更新致使的性能問題呢?問題的癥結在於,死鎖檢測要耗費大量的CPU資源。

  第一種方法就是若是你能確保這個業務必定不會出現死鎖,能夠臨時把死鎖檢測關掉。可是這種操做自己帶有必定的風險,由於業務設計的時候通常不會把死鎖當作一個嚴重錯誤,畢竟出現死鎖了,就回滾,而後經過業務重試通常就沒問題了,這是業務無損的。而關掉死鎖檢測意味着可能會出現大量的超時,這是業務有損的。

  另外一個思路是控制併發度。根據上面的分析,你會發現若是併發可以控制住,好比同一行同時最多隻有10個線程在更新,那麼死鎖檢測的成本很低,就不會出現這個問題。一個直接的想法就是,在客戶端作併發控制。可是,你會很快發現這個方法不太可行,由於客戶端不少。我見過一個應用,有600個客戶端,這樣即便每一個客戶端控制到只有5個併發線程,彙總到數據庫服務端之後,峯值併發數也可能要達到3000。

所以,這個併發控制要作在數據庫服務端。若是你有中間件,能夠考慮在中間件實現;若是從MySQL 源碼上修改,基本思路就是,對於相同行的更新,在進入引擎以前排隊。這樣在InnoDB內部就不會有大量的死鎖檢測工做了。

那麼咱們能不能從設計上優化這個問題呢?

你能夠考慮經過將一行改爲邏輯上的多行來減小鎖衝突。仍是以某東帳戶爲例,能夠考慮放在多條記錄上,好比10個記錄,某東的帳戶總額等於這10個記錄的值的總和。這樣每次要給某東帳戶加金額的時候,隨機選其中一條記錄來加。這樣每次衝突機率變成原來的1/10,能夠減小鎖等待個數,也就減小了死鎖檢測的CPU消耗。

這個方案看上去是無損的,但其實這類方案須要根據業務邏輯作詳細設計。若是帳戶餘額可能會減小,好比退貨邏輯,那麼這時候就須要考慮當一部分行記錄變成0的時候,代碼要有特殊處理。

相關文章
相關標籤/搜索