S 鎖與 X 鎖的愛恨情仇《死磕MySQL系列 四》

發表於2021-11-02

系列文章

一、原來一條select語句在MySQL是這樣執行的《死磕MySQL系列 一》

二、一生摯友redo log、binlog《死磕MySQL系列 二》

三、MySQL強人“鎖”難《死磕MySQL系列 三》

獲取MySQL各種學習資料

前言

下邊兩幅圖還熟悉吧!就是第三期文章中的前言,但上一期文章並未提及死鎖,只是引出了全域性鎖、表鎖的概念。本期文章將繼續聊聊鎖的內容。

Lock wait timeout exceeded; try restarting transaction

Deadlock found when trying to get lock; try restarting transaction

一、行鎖

行鎖的鎖粒度最小,傳送鎖衝突的概率最低,併發度也最高。

問題:MySQL的所有儲存引擎都支援行鎖嗎?

不是的,MySQL中只有Innodb儲存引擎才支援行鎖,其它的並不支援,MyIsam儲存引擎也只支援表鎖。

所以Myisam儲存引擎只能使用表鎖來解決併發,表鎖開銷小,加鎖快,鎖定粒度大,發生鎖衝突的概率最高,併發度最低。

問題:鎖粒度指的是什麼?

這種名詞不能只記名字,需要知道其代表的含義。鎖粒度指的是加鎖的範圍。

上期文章講的全域性鎖鎖的是整庫、表鎖鎖定的全表、行鎖指的是鎖定某一行或某個範圍的資料。

問題:如何加行鎖?

Innodb儲存引擎在執行update、delete、insert語句時會隱式加排它鎖,而對於select不會加任何鎖。

同樣也可以手動加鎖。

共享鎖:select * from tableName where id = 100 lock in share more

排它鎖:select * from tableName where id = 100 for update

共享鎖、排它鎖也被稱之為讀鎖、寫鎖。讀鎖與讀鎖之間不互斥,讀鎖與寫鎖、寫鎖與寫鎖之間是互斥的。

問題:為什麼要加鎖?

MySQL事務的四大特性分別是原子性、隔離性、一致性、永續性,當你瞭解完事務的四大特性之後就發現都是為了保證資料一致性為最終目的的。

常說一句話有人地方就有江湖,放在MySQL中是有鎖的地方就有事務。

所以說加鎖就是為了保證當事務結束後,資料庫的完整性約束不被破壞,從而確保資料一致性。

二、兩階段鎖

問題:兩階段鎖是什麼?

說實話,這個名字屬實很唬人,猛然間你有沒有想到另一個名詞兩階段提交。這裡回憶一下,兩階段提交是確保redo log跟binlog同時提交成功,若有一方提交失敗則回滾。

在Innodb儲存引擎中,行鎖是在需要的時候加上的,但並不是不需要了就直接釋放的,而是要等到事務結束才釋放。

案例:解釋兩階段鎖

上圖中MySQL1客戶端開啟事務並執行了兩條update語句,緊接著MySQL2開啟另一個事務執行update語句,那麼此時MySQL的更新語句會執行成功嗎?

答案肯定是不能的。

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

萬事有因必有果,有頭必有尾,鎖是開啟事務後新增的也需提交事務後解除。

現在你理解了兩階段鎖,那麼試想一下對你在寫程式碼有什麼幫助嗎?

三、理解死鎖

這幅圖是咔咔在2019年畫的,當時用這種方式來解釋死鎖對於一部分夥伴來說屬實有點繞。

錯誤的理解:之前在一個博文中看到對死鎖是這樣解釋的

現實中這樣的案例比比皆是,家裡有兩個小孩,給老大沖了一杯奶,這時老二過來也想喝。但奶嘴只有一個,此時老二隻能處於等待狀態,讓老大先喝完。這個就是死鎖。

不要把鎖等待跟死鎖一同對待,鎖等待是,一個事務中的語句新增了共享鎖,另一個事務開啟了排它鎖。此時就需要等待共享鎖的釋放,這個過程是鎖等待。而死鎖是兩個事務互相等待對方。

四、優化你的程式碼儘量防止死鎖

知道兩階段鎖後,在以後的程式碼實現中要把最可能造成鎖衝突也就是死鎖的語句放到最後邊。

問題:如何理解放到最後邊這句話?

這樣一個業務場景。

每到中午吃飯時間都是好幾個人一起出去,吃飯得付錢吧!復現一下這個流程。

1.你給商家付了10塊錢,這筆錢從你的餘額中扣。

2.給商家的賬戶新增10元。

3.記錄一條交易日誌。

在這個過程中可得知進行了兩次update操作,一次insert操作。使用為了保證交易的原子性資料的一致性此時必須得把三個操作放到一個事務。

在這三個操作中最容器造成鎖衝突的就是第2步給商家的賬戶新增錢。

所以在編碼過程中需要把第2步放到最後一步執行,保證在同樣結果下鎖住的時間最短。這樣可以在編碼的程度上儘量保證事務之間鎖等待,提高事務併發度。

五、解釋死鎖的兩種方案

第一種方式

MySQL已經給我們們提供好了,使用引數innodb_lock_wait_timeout來設定超時時間。若等待時間超過設定的值則返回超時錯誤。

在MySQL8.0版本中此值預設為50s,意味著當出現死鎖以後,被鎖住的執行緒需要50s才會自動退出,然後其它執行緒才會繼續執行。這個等待時間一般是無法接受的。

但設定時間太短會造成很多鎖等待的語句直接返回超時,造成嚴重誤傷。

重要的話再說一遍:“不要把鎖等待跟死鎖一同對待,鎖等待是,一個事務中的語句新增了共享鎖,另一個事務開啟了排它鎖。此時就需要等待共享鎖的釋放,這個過程是鎖等待。而死鎖是兩個事務互相等待對方。

第二種方式

另一個種方式,同樣MySQL也給提供了一個引數innodb_deadlock_detect,預設值為on,意思是當發現死鎖後,MySQL主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。

檢測死鎖的流程是當一個事務被堵住時,就要看它所在的執行緒是否被別的執行緒鎖住,如若沒有則繼續找下一個執行緒進行檢測,最後判斷是否出現了迴圈等待,也就是死鎖。

過程示例:新來的執行緒F,被鎖了後就要檢查鎖住F的執行緒(假設為D)是否被鎖,如果沒有被鎖,則沒有死鎖,如果被鎖了,還要檢視鎖住執行緒D的是誰,如果是F,那麼肯定死鎖了,如果不是F(假設為B),那麼就要繼續判斷鎖住執行緒B的是誰,一直走知道發現執行緒沒有被鎖(無死鎖)或者被F鎖住(死鎖)才會終止

問題:平時在開發中使用那種方案呢?

存在必合理,一般情況還是採用第二種方式,這種方式在有死鎖時是能夠快速進行處理的。

作為開發者肯定聽過一句這樣的話要麼用空間換時間,要麼用時間換空間。兩者只可兼一種。

這種方式雖可以非常迅速的處理死鎖問題,同樣也會帶來額外的負擔。

思考:帶來了那些額外的負擔?

假設你負責的業務都需要更新同一行資料。

此時按照第二種方式,當發現死鎖後,主動回滾死鎖鏈條的某一個事務,那麼,每一個進來被堵住的執行緒,都要判斷是不是由於自己的加入導致死鎖,這個時間複雜度是O(n)的操作。

假設有1000個執行緒都在更新同一行,操作的資料量是100W,檢測出來死鎖消耗資源還不怕,若最終檢測結果沒有死鎖,這個期間消耗的CPU資源是非常高的。

就如何解決這種問題再進行談論一下。

六、如何解決熱點資料的更新

為什麼要聊這個問題

使用了第二種方案來解決死鎖,熱點資料死鎖檢測會非常消耗CPU(每一個進來被堵的執行緒都會檢測是不是由於自己的加入導致的死鎖,有可能是鎖等待,但還是需要做判斷,所以非常消耗CPU),所以針對這個問題進行簡單討論一下。

咔咔在其它資料中看到有三種方案。

1.關閉死鎖檢測 2.控制併發度 3.修改MySQL原始碼對於更新同一行資料,在進入引擎之前排隊。這樣就不會出現大量的死鎖檢測

方案一:關閉死鎖檢測不考慮

這種方式會出現大量的超時,降低了使用者體驗,一般情況死鎖不會對業務產生嚴重錯誤,畢竟出現死鎖,資料大不了回滾即可。

方案二:控制併發度

可以把商家賬戶分散多個,所有的賬戶之和為賬戶餘額。

例如分了10個子賬戶,那麼出現更新同一行資料的概率就降低了10倍,這種方式在業務處理時需要簡單處理一下。防止賬戶餘額為0時使用者發起退款的邏輯處理。

這種方式還是很建議大家使用的,從設計上降低死鎖發生。

方案三:修改MySQL原始碼

大多數公司連DBA都沒有,何談存在可以修改MySQL原始碼的人,這種對於企業的成本是非常大的,而且也沒那個必要。

修改MySQL原始碼想要實現的功能是當更新同一行資料時,在進入儲存引擎之前排隊。

這種方案用佇列完全可以解決,所以並不需要從根上解決這個問題。

七、總結

本期從行鎖出發引出了兩階段鎖,明白了事務提交後才會釋放鎖。

死鎖的產生,如何從程式碼的角度來減少死鎖的產生。

MySQL也給提供了兩種方案來解決死鎖問題,對於這兩種方案咔咔也給了不同的觀點。根據自己的情況來使用。

在這期文章中並沒有演示死鎖案例,在後邊的文章中咔咔會給大家列舉幾種典型的死鎖案例。

堅持學習、堅持寫作、堅持分享是咔咔從業以來所秉持的信念。願文章在偌大的網際網路上能給你帶來一點幫助,我是咔咔,下期見。

相關文章