07 | 行鎖功過:怎麼減小行鎖對性能的影響?

在上一篇文章中,我跟你介紹了MySQL的全局鎖和表級鎖,今天咱們就來說講MySQL的行鎖。mysql

MySQL的行鎖是在引擎層由各個引擎本身實現的。但並非全部的引擎都支持行鎖,好比MyISAM引擎就不支持行鎖。不支持行鎖意味着併發控制只能使用表鎖,對於這種引擎的表,同一張表上任什麼時候刻只能有一個更新在執行,這就會影響到業務併發度。InnoDB是支持行鎖的,這也是MyISAM被InnoDB替代的重要緣由之一。sql

咱們今天就主要來聊聊InnoDB的行鎖,以及如何經過減小鎖衝突來提高業務併發度。數據庫

顧名思義,行鎖就是針對數據表中行記錄的鎖。這很好理解,好比事務A更新了一行,而這時候事務B也要更新同一行,則必須等事務A的操做完成後才能進行更新。服務器

固然,數據庫中還有一些沒那麼一目瞭然的概念和設計,這些概念若是理解和使用不當,容易致使程序出現非預期行爲,好比兩階段鎖。併發

從兩階段鎖提及

我先給你舉個例子。在下面的操做序列中,事務B的update語句執行時會是什麼現象呢?假設字段id是表t的主鍵。
性能

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

知道了這個答案,你必定知道了事務A持有的兩個記錄的行鎖,都是在commit的時候才釋放的。spa

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

知道了這個設定,對咱們使用事務有什麼幫助呢?那就是,若是你的事務中須要鎖多個行,要把最可能形成鎖衝突、最可能影響併發度的鎖儘可能日後放。我給你舉個例子。設計

假設你負責實現一個電影票在線交易業務,顧客A要在影院B購買電影票。咱們簡化一點,這個業務須要涉及到如下操做:

  1. 從顧客A帳戶餘額中扣除電影票價;

  2. 給影院B的帳戶餘額增長這張電影票價;

  3. 記錄一條交易日誌。

也就是說,要完成這個交易,咱們須要update兩條記錄,並insert一條記錄。固然,爲了保證交易的原子性,咱們要把這三個操做放在一個事務中。那麼,你會怎樣安排這三個語句在事務中的順序呢?

試想若是同時有另一個顧客C要在影院B買票,那麼這兩個事務衝突的部分就是語句2了。由於它們要更新同一個影院帳戶的餘額,須要修改同一行數據。

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

好了,如今因爲你的正確設計,影院餘額這一行的行鎖在一個事務中不會停留很長時間。可是,這並無徹底解決你的困擾。

若是這個影院作活動,能夠低價預售一年內全部的電影票,並且這個活動只作一天。因而在活動時間開始的時候,你的MySQL就掛了。你登上服務器一看,CPU消耗接近100%,但整個數據庫每秒就執行不到100個事務。這是什麼緣由呢?

這裏,我就要說到死鎖和死鎖檢測了。

死鎖和死鎖檢測

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

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

  • 一種策略是,直接進入等待,直到超時。這個超時時間能夠經過參數innodb_lock_wait_timeout來設置。
  • 另外一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其餘事務得以繼續執行。將參數innodb_deadlock_detect設置爲on,表示開啓這個邏輯。

在InnoDB中,innodb_lock_wait_timeout的默認值是50s,意味着若是採用第一個策略,當出現死鎖之後,第一個被鎖住的線程要過50s纔會超時退出,而後其餘線程纔有可能繼續執行。對於在線服務來講,這個等待時間每每是沒法接受的。

可是,咱們又不可能直接把這個時間設置成一個很小的值,好比1s。這樣當出現死鎖的時候,確實很快就能夠解開,但若是不是死鎖,而是簡單的鎖等待呢?因此,超時時間設置過短的話,會出現不少誤傷。

因此,正常狀況下咱們仍是要採用第二種策略,即:主動死鎖檢測,並且innodb_deadlock_detect的默認值自己就是on。主動死鎖檢測在發生死鎖的時候,是可以快速發現並進行處理的,可是它也是有額外負擔的。

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

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

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

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

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

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

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

可能你會問,若是團隊裏暫時沒有數據庫方面的專家,不能實現這樣的方案,能不能從設計上優化這個問題呢?

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

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

小結

今天,我和你介紹了MySQL的行鎖,涉及了兩階段鎖協議、死鎖和死鎖檢測這兩大部份內容。

其中,我以兩階段協議爲起點,和你一塊兒討論了在開發的時候如何安排正確的事務語句。這裏的原則/我給你的建議是:若是你的事務中須要鎖多個行,要把最可能形成鎖衝突、最可能影響併發度的鎖的申請時機儘可能日後放。

可是,調整語句順序並不能徹底避免死鎖。因此咱們引入了死鎖和死鎖檢測的概念,以及提供了三個方案,來減小死鎖對數據庫的影響。減小死鎖的主要方向,就是控制訪問相同資源的併發事務量。

最後,我給你留下一個問題吧。若是你要刪除一個表裏面的前10000行數據,有如下三種方法能夠作到:

  • 第一種,直接執行delete from T limit 10000;
  • 第二種,在一個鏈接中循環執行20次 delete from T limit 500;
  • 第三種,在20個鏈接中同時執行delete from T limit 500。

你會選擇哪種方法呢?爲何呢?

你能夠把你的思考和觀點寫在留言區裏,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

上期問題時間

上期我給你留的問題是:當備庫用–single-transaction作邏輯備份的時候,若是從主庫的binlog傳來一個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前的表結構。

相關文章
相關標籤/搜索