讀《MySQL技術內幕 InnoDB存儲引擎》鎖筆記
數據庫管理的是磁盤上的文件,文件系統也是對磁盤文件的管理,那麼數據庫和文件系統有什麼區別呢?
首先假設這樣一個場景,用戶開啓兩個窗口,在這2個窗口中,打開磁盤上同一個文件,起初兩個窗口中看到的文件內容是相同的,現在用戶在窗口1中修改了文件的內容,這個時候用戶在窗口2中刷新文件內容,應該看到最新的內容嗎?這得看用戶自己的選擇了。
- 選擇1:窗口2看到最新的實時的內容。
- 選擇2:窗口1中修改的文件內容保存到磁盤之後,窗口2才能看到保存之後的內容。
- 選擇3:窗口2內容一直保持不變,從打開文件之後,自始至終看到的都是相同的內容。
- 選擇4:窗口2不能打開已經在其他窗口打開的文件
要實現用戶的這種可選擇的要求,在數據庫中有一個專業的名詞——事物隔離級別,
用戶選擇 | 事物隔離級別 |
---|---|
選擇1 | 讀未提交(read-uncommitted) |
選擇2 | 不可重複讀(read-committed) |
選擇3 | 可重複讀(repeatable-read) |
選擇4 | 串行化(serializable) |
數據庫的事物的隔離級別是怎麼實現的呢?主要是通過鎖來實現。
這樣數據庫就給用戶提供了不同的策略,可供選擇,來管理文件。顯然文件系統是不具備這樣的可供選擇的策略,因此鎖的機制成爲了數據庫系統區別於文件系統的一個關鍵特性。
鎖的類型
InnoDB實現了標準的行鎖:
- 共享鎖(S Lock):允許事物持有鎖,以便讀取行數據
- 排它鎖(X Lock):允許事物持有鎖,以便更新或刪除行數據
如果事物T1在表中的第r行持有一個共享鎖,那麼另一個事物T2請求第r行的鎖,將有如下場景:
如果事物T2請求的是第r行的共享鎖,那麼將立即獲得第r行的共享鎖
如果事物T2請求的是第r行的排它鎖,那麼T2不能理解獲得鎖。
如果事物T1在第r行加了排它鎖,那麼事物T2在第r行無論請求加共享鎖還是排它鎖,都不能立即加上鎖。事物T2必須等到T1在第r行的鎖釋放纔能有機會加鎖成功。
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
從InnoDB1.0開始,在information_schema架構下添加了表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS,通過這三張表用戶可以簡單地監控當前事物並分析可能存在的鎖問題。
–查看事務
select * from information_schema.INNODB_TRX;
–查看鎖
select * from information_schema.INNODB_LOCKS;
–查看鎖等待
select * from information_schema.INNODB_LOCK_WAITS;
一致性非鎖定讀
一致性非鎖定讀:是指innoDB 存儲引擎通過多版本控制的方式來讀取當前執行時間數據庫中的行的數據。如果讀取的行正在執行delete,update操作,這是讀取操作不會因此而進行等待行上的鎖的釋放,相反innodb存儲引擎會執行讀取行的一個快照數據
快照數據是指該行的之前版本的數據,該實現是通過undo段來完成。
在事物隔離級別read-committed和repeatable-read,InnoDB存儲引擎使用非鎖定的一致性讀,在read-committed事務隔離級別下,總是讀取行的最新版本,如果行被鎖定,對於快照數據,非一致性讀總是讀取被鎖定行的最新一份快照數據。而在repeatable-read事物隔離級別下,對於快照數據,非一致性讀總是讀取事物開始時的行數據版本。
一致性鎖定讀
在某些情況下,影虎需要顯示第對數據庫讀取操作進行加鎖以保證數據邏輯的一致性,而這個要求數據庫支持加鎖語句,即使是對於select的只讀操作。InnoDB支持兩種一致性的鎖定讀操作:
- select … for update,對於讀取的行加一個X鎖
- select … lock in share mode,對於讀取的行加一個S鎖
鎖的算法
InnoDB存儲引擎有3中行鎖的算法:
- Record Lock:單個行記錄上的鎖
- Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄本身,確保索引記錄的間隙不變。間隙鎖是針對事務隔離級別爲可重複讀或以上級別而已的。
- Next-Key Lock:Gap Lock與Record Lock的結合,鎖定一個範圍,並且鎖定記錄本身
InnoDB對於行的查詢都是採用這種Next-Key Lock鎖定算法,這樣可以有效防止幻讀的發生。在默認的隔離級別下,即REPEATABLE READ下,InnoDB採用Next-key Locking機制。而在READ COMMITTED下,其僅採用Record Lock。
當InnoDB掃描索引記錄的時候,會首先對索引記錄加上行鎖(Record Lock),再對索引記錄兩邊的間隙加上間隙鎖(Gap Lock)。加上間隙鎖之後,其他事務就不能在這個間隙修改或者插入記錄。
當查詢的索引含有唯一屬性時,InnoDB會對Next-Key Lock進行優化,將其降級爲Record Lock,即僅鎖住索引本身,而不是範圍,這樣提高應用的併發性。
若是輔助索引,則情況會完全不同。
create table z(
a int,
b int,
primary key(a),
key(b)
);
insert into z select 1,1;
insert into z select 3,1;
insert into z select 5,3;
insert into z select 7,6;
insert into z select 10,8;
表z的列b是輔助索引,若在會話A中執行下面的SQL語句:
時間 | 會話A | 會話B |
---|---|---|
1 | begin; | |
2 | select * from z where b=3 for update; | |
3 | commit; |
由於有兩個索引,其需要分別進行加鎖:
- 對於聚集索引,其僅在列a等於5的索引上加上Record Lock
- 對於輔助索引,其加上的是Next-Key Lock,鎖定的範圍是(1,3]。除此之外,還會對其下一個鍵值加上Gap Lock,即還有一個範圍爲(3,6)的鎖。
因此,若在新會話B中運行下面的SQL語句,都會被阻塞:
- 第1個SQL語句:因爲在會話A中執行的SQL語句已經對聚集索引中列a=5加上了X鎖,因此執行會被阻塞
- 第2個SQL語句:主鍵插入4,沒有問題,但是插入的輔助索引值2在鎖定的範圍(1,3]內,因此執行同樣會阻塞
- 第3個SQL語句:插入的主鍵6沒有被鎖定,5也不在範圍(1,3]之間。但插入的值4在另一個鎖定的範圍(3,6]中,故也會阻塞
select * from z where a=5 lock in share mode;
insert itno z select 4,2;
insert itno z select 6,5;
時間 | 會話A | 會話B |
---|---|---|
1 | begin; | |
2 | select * from z where b=3 for update; | |
3 | begin; | |
4 | select * from z where a=5 lock in share mode; insert itno z select 4,2; insert itno z select 6,5; |
|
5 | commit; | |
6 | commit; |
而下面SQL語句不會阻塞,可以執行執行:
因爲下面的輔助索引的值都不在Next-Ket Lock的範圍內
insert itno z select 8,6;
insert itno z select 2,0;
insert itno z select 6,7;
鎖選擇
create table test(
id int,
v1 int,
v2 int,
primary key(id),
key `idx_v1`(`v1`)
)Engine=InnoDB;
- 如果更新條件沒有走索引,例如執行如下語句,此時會進行全表掃描,掃表的時候,要阻止其他任何的更新操作,所以上升爲表鎖。
update from t1 set v2=0 where v2=5;
- 如果更新條件爲索引字段,但是並非唯一索引(包括主鍵索引),例如執行如下語句,那麼此時更新會使用Next-Key Lock。
update from t1 set v2=0 where v1=9;
使用Next-Key Lock的原因:
- 首先要保證在符合條件的記錄上加上排他鎖,會鎖定當前非唯一索引和對應的主鍵索引的值
- 還要保證鎖定的區間不能插入新的數據。
- 如果更新條件爲唯一索引,則使用Record Lock(記錄鎖)
解決Phantom Problem問題
在默認的事務隔離級別下,即REPEATABLE READ下,InnoDB存儲引擎採用Next-Key Locking機制來避免Phantom Problem (幻像問題)。
Phantom Problem是指在同一事務下,連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能會返回之前不存在的行。違反了事務的隔離性,即當前事務能夠看到其他事務的結果。
InnoDB存儲引擎採用Next-Key Locking的算法避免Phantom Problem。對於SQL語句:
SELECT* FROM t WHERE a>2 FOR UPDATE;
其鎖住的不是5這單個值,而是對(2, +∞)這個範圍加了 X鎖。因此任何對於這個範圍的插入都是不被允許的,從而避免 Phantom Problem。
InnoDB存儲引擎默認的事務隔離級別是REPEATABLE READ,在該隔離級別下,其採用Next-Key Locking的方式來加鎖。而在事務隔離級別READ COMMITTED下,其僅採用Record Lock。
髒數據和髒頁的區別
髒數據和髒頁是完全不同的兩種概念:
- 髒數據是指事務對緩衝池中行記錄的修改,並且還沒有提交。是在不同事務下,當前事務可以讀取到另外事務未提交的數據,簡單來說就是可以讀到髒數據。
- 髒頁指的是在緩衝池中已經被修改的頁,但是還沒有刷新到磁盤中,即數據庫實例內存中的頁和磁盤中的頁的數據不一致。髒頁的讀取是非常正常的,髒頁是數據庫實例內存和磁盤異步造成的,這並不影響數據的一致性,髒頁最終會被刷新到磁盤中。
不可重複讀和髒讀的區別
- 髒讀讀取到的是未提交的數據
- 不可重複讀讀到的是已提交的數據,但違反了數據庫事務的一致性
死鎖
解決死鎖問題最簡單的一種方法是超時,即當兩個事務互相等待時,當一個等待時間超過設置的某一閡值時,其中一個事務進行回滾,另一個等待的事務就能繼續進行。在 InnoDB 存儲引擎中,參數 innodb_lock_wait_timeout 用來設置超時的時間。
除了超時機制,當前數據庫還都普遍採用 wait_for_graph(等待圖)的方式來進行死鎖檢測,它是一種主動的死鎖檢測方式。
wait_for_graph要求數據庫保存一下兩種信息:
- 鎖的信息鏈表
- 事物等待鏈表
通過上述鏈表可以構造出一張圖,而在這個圖中若存在迴路,就代表存在死鎖,因此資源間相互發生等待。在 wait_for_graph中,事務爲圖中的節點。而在圖中,事務 Tl 指向 T2 邊的定義爲:
- 事務 Tl 等待事務 T2 所佔用的資源
- 事務 Tl 最終等待 T2 所佔用的資源,也就是事務之間在等待相同的資源,而事務 Tl 發生在事務 T2 的後面
下面來看一個例子,當前事務和鎖的狀態如下圖所示。
在 Transaction Wait Lists 中可以看到共有 4 個事務 t1、 t2 、 t3 、 t4 ,故在 wait for graph 中應有 4 個節點。而事務 t2 對 row1 佔用 X 鎖,事務 t1 對 row2 佔用 S 鎖。事務 t1 需要等待事務 t2 中 row1 的資源,因此在 wait_for_graph 中有條邊從節點 t1 指向節點 t2 。事務 t2 需要等待事務 t1 、 t4 所佔用的 row2 對象,故而存在節點 t2 到節點 t1 、 t4 的邊。同樣,存在節點 t3 到節點 t1、t2、t4 的邊,因此最終的 wait_for_graph 如下圖所示:
根據圖形可以,t1和t2之間存在環路,所以檢測到時存在死鎖的。wait_for_graph是一種較爲主動的死鎖檢測機制,在每個事務請求鎖併發生等待時都會判斷是否存在迴路,若存在則有死鎖,通常來說 InnoDB 存儲引擎選擇回滾 undo 量最小的事務。
需要牢記:
在默認情況下InnoDB存儲引擎不會回滾曹氏引發的錯誤異常,在大部分情況下都不會對異常進行迴歸,用戶必須判斷是否需要commit還是rollback,之後再進行下一步操作。
參考:
數據庫事務和鎖(三)——INNODB_LOCKS, INNODB_LOCK_WAITS, INNODB_TRX表的簡單介紹
MySQL InnoDB鎖機制之Gap Lock、Next-Key Lock、Record Lock解析