封鎖
封鎖粒度
MySQL 中提供了兩種封鎖粒度:行級鎖以及表級鎖。
應該儘量只鎖定需要修改的那部分數據,而不是所有的資源。鎖定的數據量越少,發生鎖爭用的可能就越小,系統的併發程度就越高。
但是加鎖需要消耗資源,鎖的各種操作(包括獲取鎖、釋放鎖、以及檢查鎖狀態)都會增加系統開銷。因此封鎖粒度越小,有關鎖的操作就越頻繁,系統開銷就越大。
在選擇封鎖粒度時,需要在鎖開銷和併發程度之間做一個權衡。
封鎖類型
1. 讀寫鎖
排它鎖(Exclusive),簡寫爲 X 鎖,又稱寫鎖。
共享鎖(Shared),簡寫爲 S 鎖,又稱讀鎖。
有以下兩個規定:
1)一個事務對數據對象(數據庫、表、頁或者行) A 加了 X 鎖,就可以對 A 進行讀取和更新。加鎖期間其它事務不能對 A 加任何鎖。
2)一個事務對數據對象(數據庫、表、頁或者行) A 加了 S 鎖,可以對 A 進行讀取操作,但是不能進行更新操作。加鎖期間其它事務能對 A 加 S 鎖,但是不能加 X 鎖。
鎖的兼容關係如下:
- | X(寫鎖) | S(讀鎖) |
---|---|---|
X(寫鎖) | NO | NO |
S(讀鎖) | NO | YES |
2. 意向鎖
意向鎖表示若將上鎖的對象(數據庫,表,頁,記錄)看成一棵樹的話,則對最下層的對象上鎖,也就是對最細粒度的對象進行上鎖,那麼需要先對粗粒度的對象上鎖。
InnoDB把意向鎖設計得比較簡練,沒有上面定義那麼複雜。其意向鎖就是表級別的鎖。設計目的主要是爲了在一個事務中揭示下一行將被請求的鎖類型。
在存在行級鎖和表級鎖的情況下,事務 T 想要對錶 A 加 X 鎖,那麼就需要對錶 A 的每一行都檢測一次,這是非常耗時的。
由此,意向鎖在原來的 X/S 鎖之上引入了 IX/IS,IX/IS 都是表鎖,用來表示一個事務想要在表中的某個數據行上加 X 鎖或 S 鎖。有以下兩個規定:
1)一個事務在獲得某個數據行對象的 S 鎖之前,必須先獲得表的 IS 鎖或者更強的鎖;
2)一個事務在獲得某個數據行對象的 X 鎖之前,必須先獲得表的 IX 鎖。
意向鎖就是說在屋(比如代表一個表)門口設置一個標識,說明屋子裏有人(比如代表某些記錄)被鎖住了。另一個人想知道屋子裏是否有人被鎖,不用進屋子裏一個一個的去查,直接看門口標識就行了。
比如,事務 T 想要對錶 A 加 X 鎖時,只需要先檢測是否有其它事務在表 A門口加沒加 S/X/IX/IS 鎖,如果加了就表示有其它事務正在使用這個表或者表中某一行的鎖,因此事務 T 爲整張表加 X 鎖失敗。進入等待。
表級意向鎖與表級的X,S的兼容關係如下(IX,IS是表級鎖,不會和行級的X,S鎖發生衝突。只會和表級的X,S發生衝突):
- | X | IX | S | IS |
---|---|---|---|---|
X | NO | NO | NO | NO |
IX | NO | YES | NO | YES |
S | NO | NO | YES | YES |
IS | NO | YES | YES | YES |
疑惑解釋:
- 爲了用X鎖鎖表,是不需要申請IX的,用X鎖鎖行,才需要申請IX
- 當某個事務1要改記錄的時候,會給表加上IX鎖,接下來再給表中的記錄加上X鎖。此時,另外一個事務2若是想要對整張表加X鎖(例如alter table改變表結構的語句),不需要申請IX鎖,X鎖會和表的意向鎖IX產生衝突。
- 某個事務1要改記錄的時候,會給表加上IX鎖,接下來再給表中的記錄加上X鎖。此時,另外一個事務2若是想要對記錄加X鎖,則需要申請IX鎖,至於修改的記錄會不會和事務1的X鎖衝突則要根據隔離級別再具體分析。
- 任意 IS/IX 鎖之間都是兼容的,因爲它們只是表示想要對錶加鎖,而不是真正加鎖;
封鎖協議
封鎖能保證正確調度併發操作。爲此,在運用X鎖和S鎖這兩種基本封鎖加鎖時,還要約定規則,如何時加鎖,持續時間,何時釋放等,一般稱這些規則爲封鎖協議。
1. 三級封鎖協議——保證數據一致性
封鎖的相容矩陣:
T1/T2 | X | S | - |
---|---|---|---|
X | N | N | Y |
S | N | Y | Y |
- | Y | Y | Y |
一級封鎖協議
事務 T 要修改數據 A 時必須加 X 鎖,直到 T 結束才釋放鎖。
限制了併發的修改操作。不允許多個事務同時修改。
修改丟失主要是因爲——在你修改的時候,別的事務在中間搗亂,覆蓋了你的修改。所以一級封鎖在你修改數據的時候全程加X鎖,讓“搗蛋事務”沒有可乘之機。只能乖乖等待:
T1 | T2 |
---|---|
1 Xlock A | |
2 讀A=16 | |
Xlock A | |
3 A=A-1 | 等待 |
寫回:A=15 | 等待 |
Commit | 等待 |
Unlock A | 等待 |
Xlock A | |
讀A=15 | |
A=A-1 | |
寫回: | |
A=14 | |
Commit | |
Unlock A |
沒能解決髒讀問題:
——根據兼容矩陣,加了寫鎖和沒加鎖是相容的。所以T2在T1沒有釋放寫鎖的時候可以讀出C來。
T1 | T2 |
---|---|
1 Xlock C | |
讀C=10; C=C*2 | |
寫回C=20 | |
2 | 讀C=20 |
3 rollback | |
C恢復爲:10 | |
Unlock C | |
C成了‘髒’數據 |
沒能解決不可重複讀問題:
——自己沒加鎖,反而讓”搗蛋事務”加了寫鎖,它更有權力來搗亂了。
T1 | T2 |
---|---|
1 讀A=50 | |
讀B=100 | |
求和=150 | |
2 | XlockB |
讀B=100 | |
B=B*2 | |
寫回B=200 | |
Commit | |
Unlock B | |
3 讀A=50 | |
讀B=200 | |
求和=250 |
二級封鎖協議
在一級的基礎上,要求讀取數據 A 時必須加 S 鎖,讀取完馬上釋放 S 鎖。
讀髒數據的原因是因爲——在沒提交前的數據時不確定是否回滾的,所以禁止讓另一個事務在此事務沒提交前讀取到值即可解決。沒加S鎖的時候是可以讀到的,而加了S鎖後就可以利用S鎖和X鎖的不兼容原理。實現禁止髒讀。
但是加S鎖長度與事務長度不一致。所以還是可能出現不可重複讀和幻讀。
T1 | T2 |
---|---|
1 Xlock C | |
讀C=10 | |
C=C*2 | |
寫回C=20 | |
2 | Slock C |
3 rollback | 等待 |
C恢復爲:10 | 等待 |
Unlock C | 等待 |
獲取S鎖, Slock C | |
讀C=10 | |
Unlock C | |
Commit |
但是不可重複讀問題還是沒有解決:因爲要求讀取之後馬上釋放S鎖,讓T2的修改有機可乘。導致T1兩次讀出結果不一致
T1 | T2 |
---|---|
1 Slock A | |
Slock B | |
讀A=50 | |
讀B=100 | |
求和=150 | |
Unlock A(關鍵在於此處釋放了鎖) | |
Unlock B(關鍵在於此處釋放了鎖) | |
2 | Xlock B |
3 Slock A | 讀B=100 |
Slock B | B=B*2 |
等待 | 寫回 B=200 |
等待 | Commit |
等待 | Unlock B |
4獲得;Slock A; Slock B | |
讀 A=50 | |
讀 B=200 | |
求和=250(不一致) | |
Commit | |
Unlock A | |
Unlock B |
三級封鎖協議
在二級的基礎上,要求讀取數據 A 時必須加 S 鎖,直到事務結束了才能釋放 S 鎖。
如上面分析的,2級封鎖協議造成不可重複讀的原因就是它要求讀取完成後會立即釋放S鎖。所以3級封鎖協議要求事務結束時才釋放S鎖。便可以解決不可重複讀問題了。因爲讀 A 時,其它事務不能對 A 加 X 鎖(但一旦釋放了S鎖就可以加X鎖了)從而避免了在讀的期間數據發生改變。
因爲鎖和事務時間長度一樣,事務必須串行化執行,只要有事務在對錶進行查詢,那麼在此事務提交前,任何其他事務的修改都會被阻塞。
T1 | T2 |
---|---|
1 Slock A | |
Slock B | |
讀A=50 | |
讀B=100 | |
求和=150 | |
2 | Xlock B |
讀 A=50 | 等待 |
讀 B=100 | 等待 |
求和=150(一致) | 等待 |
Commit | 等待 |
Unlock A(事務結束釋放) | 等待 |
Unlock B(事務結束釋放) | 等待 |
獲得Xlock B | |
讀B=100 | |
B=B*2 | |
Commit | |
Unlock B |
2. 兩段鎖協議——保證並行調度可串行化
可串行化調度是指,通過併發控制,使得併發執行的事務結果與某個串行執行的事務結果相同。
規定在對任何數據進行讀寫操作之前,事務首先要獲得對該數據的封鎖;而且在釋放一個封鎖之後,事務不再獲得任何其他封鎖。
就是說加鎖和解鎖分爲兩個階段進行。
事務遵循兩段鎖協議是保證可串行化調度的充分條件。例如以下操作滿足兩段鎖協議,它是可串行化調度。
Xlock(A)…Slock(B)…Slock(C)…unlock(A)…unlock(C)…unlock(B)
但不是必要條件,例如以下操作不滿足兩段鎖協議,但是它還是可串行化調度:
Xlock(A)…unlock(A)…Slock(B)…unlock(B)…Slock(C)…unlock(C)
活鎖和死鎖
1. 活鎖
假設事務T1封鎖了某數據對象(數據庫,表,頁,行)R,事務T2也申請封鎖R,此時T2處於等待狀態;可是當事務T1釋放R後,系統允許新來的事務T3封鎖了R,T2仍然處於等待狀態;從此往後,T2一直處於等待狀態。類似於操作系統中線程的“飢餓”狀態。
解決辦法就是排隊,採用先來先服務的策略,系統按照申請數據對象R的先後順序排隊。
2. 死鎖
事務T1封鎖了數據對象R1,又申請封鎖R2,;
事務T2封鎖了數據對象R2;又申請封鎖R1.
兩者形成死鎖。
死鎖預防:
1)一次封鎖法:事務一次將所要使用的數據對象全部封鎖,否則不能執行。
2)順序封鎖法:所有事務都按照一個順序封鎖數據對象。
死鎖診斷和解除:
診斷:使用事務等待圖,它動態地反映所有事務的等待情況,併發控制子系統週期性地檢測事務等待圖,只要在圖中出現迴路,就說明存在死鎖。
解除:通常選擇處理代價最小的事務,將其回滾,釋放所有它持有的封鎖,使其他事務能繼續執行下去。
MySQL 隱式與顯示鎖定
MySQL 的 InnoDB 存儲引擎採用兩段鎖協議,會根據隔離級別在需要的時候自動加鎖,並且所有的鎖都是在同一時刻被釋放,這被稱爲隱式鎖定。
InnoDB 也可以使用特定的語句進行顯示鎖定:
SELECT … LOCK In SHARE MODE;
SELECT … FOR UPDATE;