MySQL 的鎖機制,那麼多的鎖,該怎麼區分?
楔子
本篇文章來聊一下 MySQL 的鎖,首先不光是數據庫,任何的一門高級語言也都內置了鎖機制。從本質上講,鎖是一種協調多個進程或多個線程對某一資源進行訪問的機制。
而之所以要存在鎖,是因爲在併發編程中,程序的某一部分在併發訪問的時候會導致意想不到的結果。所以這部分程序就需要用鎖保護起來,而保護起來的部分就叫做臨界區。
在 MySQL 中,按照不同的角度,可以將鎖分爲如下幾種:
這麼多的鎖,我們該怎麼區分呢?下面就來逐一回答。
髒寫是如何避免的
在區分鎖之前,先來回顧一個問題,前面我們說四種隔離級別,無論哪一種都可以避免髒寫的問題。但怎麼避免的當時卻沒有解釋,原因就是涉及到了鎖,下面來解釋一下。
再來回顧一下什麼是髒寫,假設事務 A 和 事務 B 同時對張三的賬戶餘額進行更新,初始值爲 100,那麼兩個事務拿到的也都是 100。然後事務 A 給餘額增加 100,事務 B 給餘額增加 200。理論上最終應該是 400 纔對,但如果 A 先提交 B 後提交,最終的結果卻是 300;B 先提交 A 後提交,最終的結果就是 200。
以上這種現象就是髒寫,具體表現爲:兩個事務更新同一條數據,後提交的事務將先提交的事務所做的更新給覆蓋了。
那麼如何避免呢?顯然要依賴鎖,多個事務在更新同一條數據的時候要串行更新。
所以當事務在更新數據的時候,會先看這條數據有沒有人加鎖。如果沒有,那麼該事務就會創建一個鎖,裏面包含了事務 ID(trx_id) 和等待狀態,然後將鎖和這條數據關聯在一起。
事務 A 更新數據的時候,會給數據加鎖,然後別的事務就不能再更新了。但假設這個時候又來個事務 B 也要更新這條數據,它會怎麼做呢?
首先還是判斷數據有沒有人加鎖,結果發現被事務 A 加鎖了,就知道自己不能修改這條數據。但事務 B 仍會對這條數據加鎖,只不過它處於等待狀態。
當事務 A 更新完數據,就會將自己的鎖釋放掉,並且還會去找,有沒有別人也對這條數據加了鎖。顯然它會發現該數據也被事務 B 加鎖了,於是會把事務 B 鎖裏的等待狀態修改爲 false,然後喚醒事務 B 開始執行,此時事務 B 就獲取到鎖了。
以上就是 MySQL 鎖機制的一個最基本的原理,其實就和 Python 裏面的互斥鎖是一樣的,但是基於此我們又引申出了很多不同種類的鎖。
MySQL 的讀鎖和寫鎖
先來聊聊讀鎖和寫鎖,讀鎖也被稱爲共享鎖、S 鎖,寫鎖也被稱爲獨佔鎖、排它鎖、X 鎖。而上面多個事務在更新數據時加的鎖,就是寫鎖。
那麼問題來了,如果一個事務在讀數據的時候,發現這條數據被加鎖了,那麼該事務需要繼續加鎖嗎?如果是更新數據,那麼需要加鎖,但讀數據是不需要的。因爲默認情況下,如果是讀數據,會走 MVCC 機制。
因爲讀數據可以根據 ReadView 在 undo log 版本鏈裏找一個能讀取的版本,完全不用考慮是否有別的事務在更新,ReadView 機制不允許當前事務讀取別的事務已經更新的值。所以默認情況下讀數據完全不需要加鎖,更不需要關心別的事務是否在更新數據,直接基於 MVCC 機制讀某個快照即可。
但如果就是想在讀數據的時候加鎖呢?答案是使用讀鎖,也叫 S 鎖、共享鎖。
SELECT * FROM girl
WHERE age > 16 IN SHARE MODE;
在查詢語句後面加上 IN SHARE MODE 就代表查詢數據的時候施加讀鎖。
注意:讀鎖和寫鎖是互斥的,只能有一把寫鎖或者任意多把讀鎖,也就是說如果先施加了寫鎖,就不能再施加讀鎖,因爲兩者互斥,當然更不能施加寫鎖,因爲寫鎖只能有一把。如果先施加了讀鎖,那麼不能再施加寫鎖,但是可以繼續施加讀鎖,因爲讀鎖可以有任意把。
所以可以得到如下結論:
-
更新數據的時候必然加寫鎖(MySQL 自動加),寫鎖和寫鎖是互斥的,此時別人不能更新;並且也不能加讀鎖,因爲寫鎖和讀鎖也是互斥的;但可以查詢,因爲查詢默認是不加鎖的,它走的是 MVCC 機制,會讀取快照版本;
-
查詢數據的時候可以加讀鎖,但需要手動加,默認不加鎖。並且讀鎖和寫鎖是互斥的,施加了讀鎖就不能再加寫鎖,但讀鎖和讀鎖之間是不互斥的,可以有任意把讀鎖;
不過說實話,一般開發業務系統的時候,主動給查詢加讀鎖是很少見的。另外,我們說查詢的時候默認沒有鎖,走的是 MVCC,但可以手動加讀鎖。其實除了讀鎖,查詢的時候還可以手動加寫鎖。
SELECT * FROM girl
WHERE age > 16 FOR UPDATE;
在查詢語句後面加上 FOR UPDATE 則表示給該查詢語句施加寫鎖,一般主要出現在事務查詢完畢之後還要更新數據的時候。比如該數據非常重要,事務在處理的時候不希望受到干擾。而一旦查詢的時候加了寫鎖,那麼在事務提交之前,任何人都不能更新數據了,只能在當前事務裏更新數據。而等該事務提交之後,別人才能繼續更新。
另外,讀鎖也被稱爲共享鎖和 S 鎖,寫鎖也被稱爲排它鎖、獨佔鎖和 X 鎖。這裏我們一直說的是讀鎖和寫鎖,但在 MySQL 中更常說共享鎖和獨佔鎖(排它鎖),當然意思都是一樣的,我們理解就好。
MySQL 的行鎖、表鎖和頁面鎖
基於操作類型,我們將鎖分爲讀鎖和寫鎖,如果基於操作的數據粒度劃分的話,還可以將鎖分爲行鎖、表鎖和頁面鎖。
像 IN SHARE MODE 和 FOR UPDATE 施加的都屬於行鎖,因此也可以說行級讀鎖和行級寫鎖。行鎖是針對指定行進行加鎖,比如:
-- 更新數據,MySQL會自動施加寫鎖
-- 並且只對 id = 1 的行施加寫鎖
-- 其它行不受影響
UPDATE * FROM girl
SET age = age + 1
WHERE id = 1;
行鎖的特點是開銷比較大,加鎖速度慢,可能會出現死鎖,但鎖定的粒度最小,發生鎖衝突的概率最小,併發度最高。
而表鎖則是在整個數據表上對數據進行加鎖和釋放鎖,特點是開銷比較小,加鎖速度快,一般不會出現死鎖,但鎖定的粒度比較大,發生鎖衝突的概率最高,併發度最低。
在 MySQL 中可以通過以下方式手動添加表鎖:
-- 爲 account 表增加表級讀鎖
lock table account read;
-- 爲 account 表增加表級寫鎖
lock table account write;
-- 查看數據表上增加的鎖
show open tables;
-- 刪除添加的表鎖
unlock tables;
但說實話,在工作中我們幾乎不會使用表鎖,好端端的鎖整張表幹什麼。
最後是頁面鎖,也稱爲頁級鎖,就是在頁級別對數據進行加鎖和解鎖。鎖定的粒度介於表鎖和行鎖之間,併發度一般。
工作中最常用的是行鎖,表鎖和頁面鎖基本不用,MySQL 也不會自動添加。但使用行鎖的時候,有以下幾點需要注意:
-
行鎖主要加在索引上,如果以非索引字段作爲條件進行更新,行鎖可能會變成表鎖;
-
InnoDB 的行鎖是針對索引加鎖,不是針對記錄加鎖,並且加鎖的索引不能失效,否則行鎖可能會變成表鎖;
另外行鎖、表鎖和頁面鎖都是 InnoDB 存儲引擎的特性,可能有人覺得執行 ALTER TABLE 之類的 DDL 語句施加的也是表鎖,雖然 DDL 語句和普通的增刪改語句之間也是互斥的。但其實 DDL 語句執行時施加的不是表鎖,而是元數據鎖(metadata locks),這一點要注意。
死鎖的產生和預防
雖然鎖在一定程度上能夠解決併發問題,但稍有不慎,就可能造成死鎖。發生死鎖的必要條件有 4 個,分別爲互斥條件、不可剝奪條件、請求與保持條件和循環等待條件,如下圖所示。
1)互斥條件
在一段時間內,計算機中的某個資源只能被一個進程佔用,此時如果其他進程請求該資源,則只能等待。
2)不可剝奪條件
某個進程獲得的資源在使用完畢之前,不能被其他進程強行奪走,只能由獲得資源的進程主動釋放。
3)請求與保持條件
進程已經獲得了至少一個資源,又要請求其他資源,但請求的資源已經被其他進程佔有,此時請求的進程就會被阻塞,並且不會釋放自己已獲得的資源。
4)循環等待條件
系統中的進程之間相互等待,同時各自佔用的資源又會被下一個進程所請求。例如有進程 A、進程 B 和進程 C 三個進程,進程 A 請求的資源被進程 B 佔用,進程 B 請求的資源被進程 C 佔用,進程 C 請求的資源被進程 A 佔用,於是形成了循環等待條件。
但需要注意的是,只有 4 個必要條件都滿足時,纔會發生死鎖。而處理死鎖有 4 種方法,分別爲預防死鎖、避免死鎖、檢測死鎖和解除死鎖。
-
預防死鎖:處理死鎖最直接的方法就是破壞造成死鎖的 4 個必要條件中的一個或多個,以防止死鎖的發生。
-
避免死鎖:在系統資源的分配過程中,使用某種策略或者方法防止系統進入不安全狀態,從而避免死鎖的發生。
-
檢測死鎖:這種方法允許系統在運行過程中發生死鎖,但是能夠檢測死鎖的發生,並採取適當的措施清除死鎖。
-
解除死鎖:當檢測出死鎖後,採用適當的策略和方法將進程從死鎖狀態解脫出來。
在實際工作中,通常採用有序資源分配法和銀行家算法這兩種方式來避免死鎖,有興趣可自行了解一下。
MySQL 的死鎖問題
在 MySQL 5.5.5 及以上版本中,默認存儲引擎是 InnoDB。該存儲引擎使用的是行級鎖,在某種情況下會產生死鎖問題,所以 InnoDB 存儲引擎採用了一種叫作等待圖(wait-for graph)的方法來自動檢測死鎖,如果發現死鎖,就會自動回滾一個事務。我們舉例說明:
第一步:在終端 1 中將事務隔離級別設置爲可重複讀,開啓事務後爲 account 數據表中 id 爲 1 的數據添加排他鎖。
第二步:在終端 2 中將事務隔離級別設置爲可重複讀,開啓事務後爲 account 數據表中 id 爲 2 的數據添加排他鎖。
第三步:在終端 1 中爲 account 數據表中 id 爲 2 的數據添加排他鎖。
select * from account
where id = 2 for update;
此時事務 1 會阻塞住,因爲它在等待事務 2 釋放 id = 2 的排他鎖。
第四步:在終端 2 中爲 account 數據表中 id 爲 1 的數據添加排他鎖。
我們看到死鎖了,事務 1 因事務 2 已經處於阻塞了,但此時事務 2 又因事務 1 陷入阻塞,因此出現了循環等待,所以事務 2 直接報錯、並且終止。而一旦事務 2 終止,那麼它施加的行鎖就會失效,然後事務 1 就會給 id = 2 施加行鎖成功,不再阻塞。
我們可以通過如下命令查看死鎖的日誌信息:show engine innodb status \G,或者通過配置 innodb_print_all_deadlocks(MySQL 5.6.2 版本開始提供)參數爲 ON,將死鎖相關信息打印到 MySQL 錯誤日誌中。
本文參考自:
- 儒猿技術窩《MySQL 實戰高手》
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/k9blC4joymbaLsWYnw_ymA