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

發表於2021-10-09

系列文章

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

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

前言

最近資料庫老是出現下面死鎖情況,藉著這倆種情況出發詳細的理解一下MySQL中的鎖。

Lock wait timeout exceeded; try restarting transaction

Deadlock found when trying to get lock; try restarting transaction

一、MySQL中有那些鎖

全域性鎖

根據全域性兩個字,就可以肯定的是給一個整體加上鎖。全域性鎖就是對整個資料庫例項加鎖。

對於flush tables with read lock,執行完成後整庫就處於只讀狀態,所有語句將被堵塞,包括增刪改查、建立表、修改表結構等語句。

表鎖

表鎖大家都非常熟悉了,執行命令lock tables kaka read ,kaka2 write直到unlock tables之前,其它執行緒是無法對kaka寫kaka2讀的。

執行命令的這個執行緒也只可以對kaka讀,kaka2寫。

行鎖

行鎖是在引擎層由各個引擎自己實現的。在MySQL中Innodb儲存引擎支援行鎖,若不支援行鎖意味著併發控制只能使用表鎖,對於這種引擎的表,同一張表上任何時刻只能有一個更新在執行,這就會影響到業務併發度。(由於篇幅的原因,下期細談)

二、全域性鎖

演示執行flush tables with read lock命令後資料庫處於什麼狀態。

終端1執行全域性鎖命令

埠2執行刪除操作,它不會直接執行成功,而是在埠1解鎖後返回。

這個SQL需要3分鐘的執行時間,這3分鐘就是咔咔開啟終端2並連線資料庫的時間。

現在見證了開篇所說的全域性鎖直接讓整個庫處於只讀狀態,這裡只演示了刪除操作其它的幾個操作自己嘗試一下。

在蔣老師的文章中看到全域性鎖最典型的場景是用於邏輯備份,即是將整個庫的每一個表都select儲存成文字。

現在,你想想這種場景是在什麼需要下出現的。

假如只有一個主庫,執行了全域性鎖整庫處於只讀狀態,那麼業務基本停擺,產品無法使用。

此時你會有疑問我在從庫上備份啊!備份期間,不能執行主庫同步過來的binlog的,資料量如果非常大,將引發主從延遲過大,必須進行全量備份。

以上是全域性鎖引發的負面情況,但再看備份不加全域性鎖會出現什麼問題。

相信大多數小夥伴都開發過支付類專案,接下來就用支付案例讓大家很清晰的理解備份不加全域性鎖引發的問題。

發起一個邏輯備份。如果一個使用者在備份期間購買了你公司的服務,在業務邏輯先扣除使用者餘額,然後給使用者新增你公司對應的產品。

顯然,這個邏輯沒有問題的,但在特殊案例下執行備份操作就會引發問題。

若在時間順序上先備份使用者餘額,然後使用者發起購買,接著備份使用者購買的產品表。

一個非常清晰的問題出現了,使用者餘額沒減成功但使用者卻獲得了對應的產品。

從使用者的角度出發那是賺大發了,但這種執行順序如果反過來的話就會產生不一樣的結果。

先備份使用者產品表,然後備份使用者餘額表,就會出現使用者錢花了東西沒得著,這還得了,使用者都是衣食父母這不是再割父母的韭菜。

也就是說,在備份不加鎖的話,不同表之間的執行備份的順序不同,如果某個表在備份的過程中進行了更新並且成功備份而關聯的表已經備份完成無法再進行跟新,此時就會出現資料不一致。

在MVCC那篇文章中提到了一個非常重要的概念一致性檢視(read view),一致性檢視是根據快照讀那一刻所有未提交事務的集合,前提是隔離級別為可重複。

這時你應該知道要說什麼了,沒錯就是官方大大給提供的邏輯備份工具mysqldump。

mysqldump的備份原理是通過協議連線到 MySQL 資料庫,將需要備份的資料查詢出來,將查詢出的資料轉換成對應的insert 語句,當我們需要還原這些資料時,只要執行這些 insert 語句,即可將對應的資料還原。

例如備份test庫的命令為mysqldump -uroot -p test > /backup/mysqldump/test.db

當mysqldump使用引數--single-transaction時,備份資料之前會啟動一個事物,拿到一致性檢視(read view),所以在整個備份的過程中是支援更新的。

既然有了官方大大提供的mysqldump工具為何還要使用flush tables with read lock來將整表鎖住呢?

別忘記了剛提到的可以在備份過程中進行更新,可以更新的前提是可以得到一致性檢視,獲取一致性檢視的前提是開啟事務。這裡你應該清楚,不是所有儲存引擎都支援事物。

如果有的表使用了別的儲存引擎不支援事物,那麼就只能使用flush tables with read lock方法,說到這裡希望大家儘量在建立表時都選擇Innodb儲存引擎。

看著好一會了,還能記得我們們要幹什麼嗎?需求是全庫處於只讀狀態。

如果你搭建過MySQL的主從架構,就會知道主庫用來寫資料,從庫用來讀資料並且從庫不支援寫入操作,可以實現這樣的效果都是來自於引數readonly。

同樣執行set global readonly=true也可以達到整庫只讀狀態,那麼為什麼從一開始沒有給大家說這個方案,那是有原因的。

一是,剛剛提到的搭建主從架構需要使用readonly來判斷主庫於從庫。

二是,在異常處理的方式不同。如果使用flush talbes with read lock命令客戶端異常後MySQL會自動釋放全域性鎖,讓整個庫回到正常狀態。而整庫設定為readonly後,一旦發生異常就會一直處於只讀狀態,導致整庫長時間處於不可寫狀態。

所以說資料庫一旦加上全域性鎖後資料的增刪改、修改表結構、修改欄位等操作都會被鎖住。

三、表鎖

表鎖跟全域性鎖釋放的命令一致unlock tables,同樣客戶端斷開的時候也會自動釋放。

在老一輩的革命前輩處理併發都是用的表鎖,應該都知道鎖表的影響雖不及鎖庫影響大,但在今天鎖的粒度已經支援到行鎖了(前提是使用Innodb儲存引擎,就沒必要再使用行鎖來處理併發了。

再來看錶鎖中的另一位哥們“後設資料鎖”(metalock)簡稱“MDL”,這個鎖估計很少人知道,因為在實際開發過程中是不會有實際的語法來開啟或關閉。

這個特性是在MySQL5.5版本後引入的,就是為了解決A執行緒正在查詢一個表的資料,在這期間B執行緒修改了表的資料結構,那麼就會造成查詢的結果跟表結構對不上,這肯定是不行的。

當你訪問一個表時會預設加上MDL寫鎖,不管在任何時候記住讀鎖於讀鎖之間不互斥,讀鎖與寫鎖,寫鎖與寫鎖之間互斥,知道行鎖的共享鎖、排它鎖也是這麼個理。

那麼MDL 不需要顯示呼叫,那它是在什麼時候釋放的?

回答是:“MDL 是在事務提交後才會釋放,這意味著事務執行期間,MDL 是一直持有的。”

那麼看一個場景。

首先,執行緒A開啟事務並執行查詢語句時,對錶加上了MDL鎖。

然後,執行緒B執行的是查詢,並不會堵塞住,因為讀與讀並不衝突。

接著,執行緒C修改表結構,此時的執行緒A還未提交事務,MDL還未釋放,這時的執行緒因無法獲取到MDl寫鎖,就會被阻塞。

最後執行緒D執行查詢會發生什麼呢?

答案是堵塞。

到這裡按照正常的邏輯,執行緒C沒有獲取到MDL的寫鎖,執行緒D是可以申請到MDL讀鎖的,那為什麼還會堵塞呢!

這是因為申請MDL鎖的操作會形成一個佇列,佇列中寫鎖獲取優先順序高於讀鎖,一旦出現MDL寫鎖等待,會阻塞後續該表的所有CURL操作

到這裡你有沒有後背發涼,一旦你在一個未提交事務之後執行了DDL操作,那麼等到的結果就是MySQL掛掉,客戶端會有重試機制,DDL後所有CURD會在超時後重新發起請求,這個庫的執行緒會很快爆滿。

既然這樣如何給表安全的執行DDL操作呢?

首先,必須解決到長事務,事務不提交MDL鎖就無法釋放。

然後,在MySQL系統表裡找到infomation_schema庫中的innodb_trx,可以檢視當前正在執行中的事務ID,這個表在事務那期文章中也沒少提。

接著,你是不是想kill掉這些長事務然後執行DDL不就得了。

試想一下,當你kill掉的下一刻一個新的事務又進來了,同時你又執行了DDL操作,後果是什麼應該清楚了哈!這種操作肯定是不行的。

官方大大怎麼會允許這種情況發生呢!

於是當你執行DDL操作時alter table kaka wait 30 add name可以加一個等待時間,如果在這個等待時間拿到MDL寫鎖最好,拿不到也不能堵塞後邊的業務邏輯,先放棄。再重試執行這個命令。

四、總結

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

相關文章