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


系列文章

一、原來一條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也給提供了兩種方案來解決死鎖問題,對於這兩種方案咔咔也給了不同的觀點。根據自己的情況來使用。

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

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


免責聲明!

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



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