MySQL行鎖


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

 

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

 

從兩端鎖說起

 

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

 

事務A持有的兩個記錄的行鎖,都是在commit的時候才釋放的。

 

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

 

如果你的事務中需要鎖多個行,要把最可能造成鎖沖突、最可能影響並發度的鎖盡量往后放。

舉個例子,假設你負責實現一個電影票在線交易業務,顧客A要在影院B購買電影票。我們簡化一點,這個業務需要涉及到以下操作:

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

  2. 給影院B的賬戶余額增加這張電影票價;
  3. 記錄一條交易日志。 

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

 

根據兩階段鎖協議,不論你怎樣安排語句順序,所有的操作需要的行鎖都是在事務提交的時候才釋放的。所以,如果你把語句2安排在最后,比如按照3、1、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利用率很高,但是每秒卻執行不了幾個事務。

 

怎么解決由這種熱點行更新導致的性能問題呢?

 

1.一種頭痛醫頭的方法,就是如果你能確保這個業務一定不會出現死鎖,可以臨時把死鎖檢測關掉。

  但是這種操作本身帶有一定的風險,因為業務設計的時候一般不會把死鎖當做一個嚴重錯誤,畢竟出現死鎖了,就回滾,然后通過業務重試一般就沒問題了,這是業務無損的。而關掉死鎖檢測意味着可能會出現大量的超時,這是業務有損的。

2.另一個思路是控制並發度。

  根據上面的分析,你會發現如果並發能夠控制住,比如同一行同時最多只有10個線程在更新,那么死鎖檢測的成本很低,就不會出現這個問題。一個直接的想法就是,在客戶端做並發控制。但是,你會很快發現這個方法不太可行,因為客戶端很多。我見過一個應用,有600個客戶端,這樣即使每個客戶端控制到只有5個並發線程,匯總到數據庫服務端以后,峰值並發數也可能要達到3000。

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

  你可以考慮通過將一行改成邏輯上的多行來減少鎖沖突。還是以影院賬戶為例,可以考慮放在多條記錄上,比如10個記錄,影院的賬戶總額等於這10個記錄的值的總和。這樣每次要給影院賬戶加金額的時候,隨機選其中一條記錄來加。這樣每次沖突概率變成原來的1/10,可以減少鎖等待個數,也就減少了死鎖檢測的CPU消耗。

  這個方案看上去是無損的,但其實這類方案需要根據業務邏輯做詳細設計。如果賬戶余額可能會減少,比如退票邏輯,那么這時候就需要考慮當一部分行記錄變成0的時候,代碼要有特殊處理。

 

小結

如果你的事務中需要鎖多個行,要把最可能造成鎖沖突、最可能影響並發度的鎖的申請時機盡量往后放。

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

 

問題

如果你要刪除一個表里面的前10000行數據,有以下三種方法可以做到:

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

第二種合適。

第一種方式(即:直接執行delete from T limit 10000)里面,單個語句占用時間長,鎖的時間也比較長;而且大事務還會導致主從延遲。

第三種方式(即:在20個連接中同時執行delete from T limit 500),會人為造成鎖沖突。

 

參考丁奇老師的《MySQL實戰》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM