MySQL鎖系列文章已經鴿了挺久了,最近趕緊擠了擠時間,和大家聊一聊MySQL的鎖。
只要學計算機,「鎖
」永遠是一個繞不過的話題。MySQL鎖也是一樣。
一句話解釋MySQL鎖:
MySQL鎖是解決資源競爭的一種方案。
短短一句話卻包含了3點值得我們注意的事情:
- 對什么資源進行競爭?
- 競爭的方式(或者說情形)有哪些?
- 鎖是如何解決競爭的?
這篇文章開始帶你循序漸進地理解這幾個問題。
1. 資源的競爭方式
MySQL對資源的操作無非就是 讀 、寫兩種方式,但是由于事務并發執行的存在,因此對同一資源的并發訪問存在3種形式:
- 讀—讀:并發事務同時讀取相同資源。由于讀操作不會改變資源本身,因此 這種情況下并不存在并發安全性問題 。
- 讀—寫/寫—讀:一個事務對資源進行讀操作,另一個事務對資源進行寫操作。
- 寫—寫:并發事務同時對同一個資源進行寫操作。
2. 讀—寫/寫—讀下的問題
假設一種情形,一個事務先對某個資源進行讀操作,然后另一個事務再對該資源進行寫操作,如果兩個事務到此為止,必然不會導致并發問題。
可是事務這種東西,一般情況下就是包含有很多個子操作啊。
2.1. 幻讀
想象一下啊,假設事務T1
和T2
并發執行,T1
先查找了所有name
為「王剛蛋」的用戶信息,此時發現擁有這個硬漢名字的用戶只有一個。然后T2
插入了一個同樣叫做「王剛蛋」的用戶的信息,并且提交了。
幻讀
2.2. 不可重復讀
再來,同樣是T1
和T2
兩個事務,T1
通過id = 1
查詢到了一條數據,然后T2
緊接著UPDATE
(DELETE
也可以)了該條記錄,不同的是,T2
緊接著通過COMMIT
提交了事務。
此時,T1
再次執行相同的查詢操作,會發現數據發生了變化,name
字段由「王剛蛋」變成了「蟬沐風」。
如果一個事務讀到了另一個已提交事務修改過的(或者是刪除的)數據,而導致了前后兩次讀取的數據不一致的情況,這種事務并發問題叫做 不可重復讀 。
2.3. 臟讀
事情還沒結束,假設T1
和T2
都要訪問user_innodb
表中id
為1
的數據,不同的是T1
先讀取數據,緊接著T2
修改了數據的name
字段,需要注意的是,T2
并沒有提交!
此時,T1
再次執行相同的查詢操作,會發現數據發生了變化,name
字段由「王剛蛋」變成了「蟬沐風」。
如果一個事務讀到了另一個未提交事務修改過的數據,而導致了前后兩次讀取的數據不一致的情況,這種事務并發問題叫做 臟讀 。
2.4. 鎖與MVCC的關系
總結一下:我們在讀—寫,寫—讀的情況下會遇到3種讀不一致性的問題,臟讀、不可重復讀以及幻讀。
那寫—寫呢?很顯然,在不做任何措施的情況下,并發會出現更大的問題。那該怎么辦呢?
一切的并發問題都可以通過串行化解決,但是串行化效率太低了!
再優化一下,一切并發問題都可以通過加鎖來解決,這種方案我們稱為 基于鎖的并發控制 ( Lock Bases Concurrency Control , LBCC )!但是在讀多寫少的環境下,客戶端連讀取幾條記錄都需要排隊,效率還是太低了!
因此,MySQL的設計者為事務之間的隔離性提供了不同的級別,使得開發者可以根據自己的業務場景設置不同的隔離級別,來解決(或者部分解決)讀—寫/寫—讀下的讀一致性問題,而不是一上來就加鎖。
這種機制叫做MVCC
,如果你對這個概念不是很了解,我建議你暫停一下,讀一下我的事務的隔離性與MVCC這篇文章,寫得賊好!!(自賣自夸一下)
那有了MVCC是不是在讀—寫/寫—讀的情況下就不需要鎖了呢?那也不是。
MVCC解決的是讀—寫/寫—讀中“ 比較純粹的讀 ”遇到的一致性問題,原諒我,這是我自己編的詞兒。那什么是不純粹的?拿存款業務舉個例子。
假設陀螺要存一筆錢,系統需要先把陀螺的余額讀出來,然后在余額的基礎上加上本次存款的金額,最后再寫入到數據庫中。在將余額讀出來之后,如果不想讓其他事務繼續訪問該余額,直到整個存款事務完成之后,其他事務才可以對該余額繼續進行操作,這種情況下就必須為余額的讀取操作添加鎖。
再總結一下: MVCC是MySQL默認的解決讀—寫/寫—讀下一致性問題的方式,不需要加鎖。而鎖是實現一致性的最終兜底方案,在某些特殊場景下,鎖的使用不可避免 。
說得更準確一點,MVCC是MySQL在
READ COMMITTED
、REPEATABLE READ
這兩種隔離級別之下執行普通SELECT
操作時默認解決一致性問題的方式。具體為什么只是這兩種隔離級別,建議你看看事務的隔離性與MVCC。
2.5. 鎖與事務的關系
事務是多個操作的集合,比如我們可以把「把大象裝冰箱」這件事情作為一個事務。
事務有A
(原子性)、C
(一致性)、I
(隔離性)、D
(持久性)4大特性,而鎖就是實現隔離性的其中一種方案(比如還有MVCC等方案)。
事務的隔離性針對不同場景需求又實現了不同的隔離級別,不同的隔離級別下,事務使用鎖的方式又會有所不同。舉個例子。
在READ COMMITTED
、REPEATABLE READ
這兩種隔離級別之下,SELECT
操作是不需要加鎖的,直接使用MVCC機制即可滿足當前隔離級別的需求。但是在SERIALIZABLE
隔離級別,并且在禁用自動提交時(autocommit=0),MySQL會將普通的SELECT
語句轉化為SELECT ... LOCK IN SHARE MODE
這樣的加鎖語句,如果你看不懂這句話也沒關系,你只需要知道MySQL自動加鎖了就行,更詳細的下文再說。
另外,一個事務可能會加很多個鎖,但是某個鎖一定只屬于一個事務。這就好比一個管理員可以管理多個保險柜,一個保險柜一定只被一個管理員管理。
3. 寫—寫情況
寫—寫的情況下肯定要加鎖的了,所以接下來終于要聊一聊鎖了。
我們首先研究一下鎖住的東西的大小,也就是鎖的粒度。
4. 鎖的粒度
舉一個非常應景的例子。疫情防控的時候,是封鎖整個小區還是封鎖某棟樓的某個單元,這完全是兩種概念。
對應到MySQL鎖的粒度,那就是表鎖
和行鎖
。
很容易想到,封鎖小區的行為遠比封鎖某棟樓某單元的行為粗曠,因此,
從鎖定粒度上來看,表鎖 > 行鎖
直接堵住小區的門口要比進入小區找到具體某棟樓的某個單元要快不少,因此,
從加鎖效率上來看,表鎖 > 行鎖
直接鎖住小區大概率會影響其他樓居民的正常生活和各種社會活動的開展,而鎖住某棟樓某單元頂多影響這一個單元的居民的生活,因此,
從沖突概率來看,表鎖 > 行鎖
從并發性能來看,表鎖 < 行鎖
MySQL支持很多存儲引擎,而不同的存儲引擎對鎖的支持也不盡相同。對于MyISAM
、MERGE
、MEMORY
這些存儲引擎而言,只支持表鎖;而InnoDB
存儲引擎既支持表鎖也支持行鎖,下文討論的所有內容均針對InnoDB存儲引擎。
說完鎖的粒度,還有一件事情需要我們仔細考慮一下。上文說過,READ COMMITTED
、REPEATABLE READ
這兩種隔離級別之下,SELECT
操作默認采用MVCC機制就可以了,壓根兒不需要加鎖,那么問題來了,萬一我就是想加鎖呢?
你可能會說,“簡單啊,那就加鎖!把數據鎖死!除了我誰也別動!”
很好,但是對于大部分讀—讀而言,由于不會出現讀一致性問題,所以不讓其他事務進行讀操作并不合理。
你可能又說,“那行吧,那就讓讀操作加鎖的時候允許其他事務對鎖住的數據進行讀操作,但是不允許寫操作。”
嗯,想得確實更細致了一些。但是再想想我上文中舉過的陀螺存錢的例子,有時候SELECT
操作需要獨占數據,其他事務既不能讀,更不能寫。
我們把這種共享和排他的性質稱為鎖的基本模式。
5. 鎖的基本模式
5.1. 共享鎖
共享鎖(Shared Lock),簡稱S
鎖,可以同時被多個事務共享,也就是說,如果一個事務給某個數據資源添加了S
鎖,其他事務也被允許獲取該數據資源的S
鎖。
由于S
鎖通常被用于讀取數據,因此也被稱為 讀鎖 。
那怎么給數據添加S
鎖呢?
我們可以用 SELECT ... LOCK IN SHARE MODE;
的方式,在讀取數據之前就為數據添加一把S
鎖。如果當前事務執行了該語句,那么會為讀取到的記錄添加S
鎖,同時其他事務也可以使用SELECT ... LOCK IN SHARE MODE;
方式繼續獲取這些數據的S
鎖。
我們通過以下的例子驗證一下S
鎖是否可以重復獲取。
5.2. 排他鎖
排他鎖(Exclusive Lock),簡稱X
鎖。只要一個事務獲取了某數據資源的X
鎖,其他的事務就不能再獲取該數據的X
鎖和S
鎖。
由于X
鎖通常被用于修改數據,因此也被稱為 寫鎖 。
X
鎖的添加方式有兩種,
- 自動添加
X
鎖
我們對記錄進行增刪改時,通常情況下會自動對其添加X
鎖。 - 手動加鎖
我們可以用SELECT ... FOR UPDATE;
的方式,在讀取數據之前就為數據添加一把X
鎖。如果當前事務執行了該語句,那么會為讀取到的記錄添加X
鎖,這樣既不允許其他事務獲取這些記錄的S
鎖,也不允許獲取這些記錄的X
鎖。
我們用下面的例子驗證一下X
鎖的排他性。
通常情況下,事務提交或結束事務時,鎖會被釋放。
-
計算機
+關注
關注
19文章
7494瀏覽量
87965 -
MySQL
+關注
關注
1文章
809瀏覽量
26575 -
MVCC
+關注
關注
0文章
13瀏覽量
1470
發布評論請先 登錄
相關推薦
評論