開發多用戶、資料庫驅動的應用時,最大的難點是:一方面要最大程度的利用資料庫的併發訪問,一方面還要確保每個用戶能以一致的方式讀取和修改數據,為此有了鎖的機制。 6.1 什麼是鎖 鎖機制用於管理對共用資源的併發訪問。InnoDB除了會在行級別上對錶數據上鎖,也會在資料庫內部其他多個地方上鎖,從而允許對多 ...
開發多用戶、資料庫驅動的應用時,最大的難點是:一方面要最大程度的利用資料庫的併發訪問,一方面還要確保每個用戶能以一致的方式讀取和修改數據,為此有了鎖的機制。
6.1 什麼是鎖
鎖機制用於管理對共用資源的併發訪問。InnoDB除了會在行級別上對錶數據上鎖,也會在資料庫內部其他多個地方上鎖,從而允許對多種不同資源提供併發訪問。例如,操作緩衝池中的LRU列表,刪除、添加移動LRU列表上的元素等。
InnoDB 鎖的實現和 Oracle資料庫非常類似,提供一致性的非鎖定讀、行級鎖支持,行級鎖沒有相關的額外開銷,並可以得到併發性和一致性。
6.2 lock 與 latch(閂)
latch 一般稱為閂鎖(輕量級),因為其要求鎖定的時間非常短,若持續時間長,性能會很差。在 InnoDB中,latch 又可以分為 mutex 和rwlock (讀寫鎖)。目的是用來保證併發線程操作臨界資源的正確性,並且通常沒有死鎖檢驗。
lock 一般鎖定的是資料庫中的對象,如表、頁、行。並且lock的對象僅在事務 commit 或 rollback後釋放。
lock | latch | |
對象 | 事務 | 線程 |
保護 | 資料庫記憶體 | 記憶體數據結構 |
持續時間 | 整個事務過程 | 臨界資源 |
模式 | 行鎖、表鎖、意向鎖 | 讀寫鎖、互斥鎖 |
死鎖 | 通過 wait for graph、time out 機制進行死鎖檢測與處理 | 無死鎖檢測與處理機制,僅通過應用程式加鎖的順序保證無死鎖的發生 |
存在於 | lock Manager的哈希表中 | 每個數據結構的對象中 |
6.3 InnoDB存儲引擎中的鎖
6.3.1 鎖的類型
InnoDB 實現了以下兩種標準的行級鎖:
- 共用鎖,S Lock,允許事務讀一行數據
- 排它鎖, X Lock,允許事務刪除或更新一行數據
S和X 鎖都是行鎖,相容是指對同一行記錄的相容性情況。
InnoDB 支持多粒度鎖定,這種鎖定支持行級別的鎖和表級別的鎖同時存在,即意向鎖,意向鎖是將鎖定的對象分為多個層次,意向鎖意味著事務希望在更細粒度上進行加鎖。
如果需要對頁上的記錄r加X鎖,需要分別對資料庫A、表、頁上意向鎖IX,最後對r上X鎖。若其中任意一個部分導致等待,那麼該操作需要等待粗粒度的鎖完成。InnoDB支持意向鎖比較簡單,其意向鎖就是表級別的鎖,主要是為了在一個事務中揭示下一行將被請求的鎖類型。
- 意向共用鎖(IS Lock),事務想要獲得表中某幾行的共用鎖
- 意向排它鎖(IX Lock),事務想要獲得表中某幾行的排它鎖
InnoDB支持的是行級別的鎖,因此意向鎖不會阻塞全表掃描以外的任何請求。
IS | IX | S | X | |
IS | 相容 | 箭容 | 相容 | 不相容 |
IX | 相容 | 相容 | 不相容 | 不相容 |
S | 相容 | 不相容 | 相容 | 不相容 |
X | 不相容 | 不相容 | 不相容 | 不相容 |
6.3.2 一致性非鎖定讀
一致性的非鎖定讀(consistent nonlocking read)是指 InnoDB 通過多版本併發控制(multi versioning Concurrency control)的方式來讀取當前執行時間資料庫中行的數據。如果此時,讀取的行正在執行 delete 或 update,這時讀取操作不會因此等待行上的鎖釋放,相反,InnoDB會去讀取行的一個快照數據(該實現是通過 undo 段來完成),而undo段是用來在事務中回滾數據,因此沒有額外開銷,此外,讀取快照數據是不需要上鎖的,因為沒有事務需要對歷史數據進行修改。
一致性的非鎖定讀,是InnoDB的預設讀取方式(Repeatable隔離級別),即讀取不會占用和等待表上的鎖。
一個行記錄不止一個快照數據,一般稱這種技術為行多版本併發控制技術,在事務隔離級別為 READ COMMITED 和 REPEATABLE READ下,InnoDB使用的是非鎖定的一致性讀。
在 READ COMMITED事務隔離級別下,對於快照數據總是讀取被鎖定行的最新一份的快照數據,從資料庫理論的角度看,違反了ACID中的隔離性I。而 REPEATABLE READ 讀取的是事務開始時的行數據版本。
時間 | 會話A | 會話B |
1 | BEGIN | |
2 | select * from t where id=1 | |
3 | BEGIN | |
4 |
update t set id=3 where id=1 此時,事務沒有提交,加X鎖,會話A的查詢5,使用MVCC查詢結果 |
|
5 | select * from t where id=1 | |
6 | COMMIT; | |
7 | select * from t where id=1 | |
8 | COMMIT; |
在 READ COMMITED 隔離級別下,1,5 返回id=1的數據,7返回null,因為會話B提交後,id=1的數據被更新為3,此時的數據是最新的。而在REPEATABLE READ 下,1,5,7 總是返回id=1 的數據,因為總是讀取事務開始前的數據。
6.3.3 一致性鎖定讀
某些情況下,用戶需要顯式的對資料庫讀取操作進行加鎖以保證數據的一致性。而這要求資料庫支持加鎖語句,即使是對於select 的查詢語句。InnoDB對於 select語句支持兩種一致性的鎖定讀操作:
- select …… for update
- select …… lock in share mode
select …… for update 對讀取的行加X鎖,其他事務不能再對該行加任何鎖。select …… share in mode 對讀取的行記錄加S鎖,其他事務可以向被鎖定的行加S鎖,但不能加X鎖,X鎖只能阻塞。
select …… for update 和 select …… share in mode必須在事務中,事務提交,鎖釋放。
6.3.4 自增長與鎖
在 InnoDB的記憶體結構中,對每個含有自增長值的表都有一個自增長計數器。執行如下的語句得到計數器的值:
select MAX(auto_inc_col) from t for update;
插入操作會根據這個自增長的計數器值加1 賦予自增長列。這個實現方式被稱為 AUTO-INC Locking、這種鎖其實採用一種特殊的表鎖機制,為了提高插入性能,鎖不是在事務完成後釋放,而是完成對自增長值的插入的SQL語句後立即釋放。
雖然 AUTO-INC Locking在一定程度上提高了併發性,但對於有自增長值的列的併發插入性能較差,事務必須等待前一個插入的完成(雖然不需要等待事務的完成),其次,對於 Insert…… select 的大數據量的插入會影響插入的性能,因為另一個事務會被阻塞。
MySQL 5.1.22 版本開始,InnoDB提供了一種輕量級互斥量的自增長的實現機制。從該版本開始,InnoDB提供了一個參數 innodb_autoinc_lock_mode來控制自增長的模式,該參數預設為1.
innodb_autoinc_lock_mode | 說明 |
0 | MySQL5.1.22版本之前的實現方式,即通過 AUTO-INC lock方式,因為有了新的自增長方式,所以該值不應該是用戶的首選項 |
1 |
預設值,對於”simple inserts“,該值會使用互斥量對記憶體中的計數器進行累加的操作。對於”bulk inserts“,還是使用傳統的 AUTO-INC locking方式。在這種配置下,如果不考慮回滾操作,對於自增值的增長還是連續的。並且在這種方式想,statement-based 方式的replication還是能很好的工作。 註:如果已經使用 AUTO-INC locking方式,而這時需要再進行”simple inserts“操作時,還是需要等待”AUTO-INC locking“的釋放 |
2 | 在該模式下,對所有的”insert-likes“自增長值的產生都是通過互斥量,而不是 AUTO-INC locking,顯然,這是性能最高的方式,然而,會帶來一些問題,因為併發的存在,在每次插入時,自增長的值可能不是連續的,此外,重要的是,基於 Statement-based Replication會有問題。因此,使用這個模式,任何時候都應該使用 row-base replication,這樣才能保證最大的併發及 replication主從數據的一致性。 |
插入類型 | 說明 |
insert-like | 所有的插入語句,包括 insert、replace、insert……select、replace……select、load data等 |
simple inserts | 能在插入前就確定插入行數的語句,包括insert,replace等,不包含 insert …… on duplicate key update這類SQL |
bulk inserts | 在插入前不能確定插入行數的語,如 insert……select,replace……select、load data等 |
mixed-mode inserts | 插入中有一部分的值是自增長的,一部分是可以確定的,如insert into t (e1,e2) values (1,'a'),(NULL,'b'),(3,'c'),也可以是 insert …… on duplicate key update這類SQL |
另外,在 InnoDB中,自增長的列必須是索引,同時是索引的第一個列,否則MySQL會拋出異常。
6.3.5 外鍵和鎖
外鍵主要用於引用完整性的約束檢查,在InnoDB中,對於一個外鍵列,如果沒有顯示的對這個列添加索引,InnoDB會自動對其加索引,避免表鎖。
對於外鍵的插入或更新,需要先查詢父表,但是對於父表的查詢不是使用一致性非鎖定讀(MVCC),因為這樣會發生數據不一致的問題,使用的是 select ……lock in share mode,主動為付表加S鎖,如果這時父表上已有X鎖,則阻塞。
時間 | 會話A | 會話B |
1 | BEGIN | |
2 | delete from parent where id=5 | |
3 | BEGIN | |
4 |
insert into child select 2,5 #第二列是外鍵,執行時被阻塞waiting |
6.4 鎖的演算法
6.4.1 行鎖的三種演算法
InnoDB 有三種行鎖演算法,分別是
- Record Lock:單個記錄上的鎖
- Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄本身
- Next-Key Lock:Gap Lock + Record Lock,鎖定一個範圍,並鎖定記錄本身
Record Lock 總是鎖住索引記錄,如果 InnoDB在建立時沒有設置任何一個索引,那麼 InnoDB會使用隱士的主鍵來鎖定。
InnoDB對於行的查詢都是採用 Next-Key Lock,其目的是為瞭解決幻讀 Phantom Problem,是謂詞鎖的一種改進。
當查詢的索引含有唯一屬性時,InnoDB 會對Next-Key Lock優化,降級為 Record Key,以此提高應用的併發性。
如前面介紹的。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;
其中b是輔助索引,此時執行 select * from z where b=3 for update;
此時,sql 語句通過索引列b進行查詢,因此其使用傳統的 next-key lock 進行加鎖,並且由於有兩個索引,其需要分別進行鎖定。對於聚集索引,僅對列a=5的索引加record lock。而輔助索引,加的是next-key lock,鎖定的是(1,3)範圍,特別註意的是,InnoDB還會對輔助索引下一個鍵值加上 gap lock,即還有一個(3,6)範圍的鎖。因此,一下SQL都會被阻塞:
select * from z where a=5 lock in share mode; insert into z select 4,2; insert into z select 6,5;
從上面可以看出,gap lock的作用是為了阻止多個事務將記錄插入到同一個範圍內,而這會導致幻讀問題的產生。用戶可以通過以下兩種方式來關閉 Gap Lock:
- 將事務隔離級別改為 READ COMMITED
- 將參數 innodb_locks_unsafe_for_binlog設為1
在上述的配置下,除了外鍵約束和唯一性檢查依然需要的Gap Lock,其餘情況僅使用 Record Lock進行鎖定,需要牢記的是,上述配置破壞了事務的隔離性,並且對 replication可能會導致不一致。且從性能上看,READ COMMITED也不會優於預設的 READ REPEATABLE;
在 InnoDB中,對Insert的操作,其會檢查插入記錄的下一條記錄是否被鎖定,若已鎖定,則不允許查詢。對於上面的例子,會話A已經鎖定了表中b=3的記錄,即已鎖定了(1,3)的範圍,這時如果在其他會話中進行如下的插入同樣會導致阻塞
insert into z select 2,2;
因為檢測到輔助索引列b上插入2的記錄時,發現3已經被索引,而將插入修改為如下值,可以立即執行:
insert into z select 2,0;
最後,對於唯一鍵值的鎖定,next-key lock降級為record ke僅存在於查詢所有的唯一索引列。若唯一索引由多個列組成,而查詢僅是查找多個唯一索引列中的一個,那麼查詢其實是range類型,而不是point查詢,故InnoDB依然採用 next-key lock進行鎖定。
6.4.2 解決 Phantom Problem
幻讀指的是在同一個事務下,連續執行兩次相同的SQL語句可能返回不一樣的結果,第二次的SQL語句可能會返回之前不存在的行。
InnoDB採用 next-key lock 的演算法解決了 Phantom Problem,對 select * from t where id > 2 for update,鎖住的不單是5這個值,而是對(2,+∞)這個範圍加了X鎖。因此,對這個範圍的插入是不允許的,從而避免幻讀。
時間 | 會話A | 會話B |
1 |
set session tx_isolation = 'READ-COMMITED' |
|
2 | BEGIN | |
3 |
select * from t where a>2 for update; ***********1 row ************* a:4 |
|
4 | BEGIN | |
5 | insert into t select 4 | |
6 | COMMIT; | |
7 |
select * from t where a>2 for update; ***********1 row ************* a:4 ***********2 row ************* a:5 |
REPEATABLE READ 採用的是 next-key locking加鎖。而 READCOMMITED 採用的是 record lock .
此外,用戶可以通過 InnoDB的 next-key lock在應用層面實現唯一性的檢查:
select * from table where col=xxx lock in share mode; if not found any row : #unique for insert value insert into table values(……);
如果用戶通過一個索引查詢一個值,並對該行加上了S lock,那麼即使查詢的值不存在,其鎖定的也是一個範圍,因此若沒有返回任何行,那麼新插入的值一定是唯一的。
那,如果在第一步select lock in share mode時,有多個事務併發操作,那麼這種唯一性檢查是否會有問題,其實不會,因為會發生死鎖。只有一個事務會成功,其他的事務會拋出死鎖錯誤。
6.5 鎖問題
6.5.1 臟讀
臟數據是指未提交的數據,如果讀到了臟數據,即一個事務可以讀到另一個事務未提交的數據,則顯然違反了資料庫的隔離性。
臟讀指的是在不同事務下,當前事務可以讀到另外事務未提交的數據,即臟數據。
時間 | 會話A | 會話B |
1 |
set @@tx_isolation = 'read-ncommited' |
|
2 |
set @@tx_isolation = 'read-ncommited' |
|
3 |
BEGIN |
|
4 |
select * from t ; **********1 row ************* a:1 |
|
5 | insert into t select 2; | |
6 |
select * from t ; **********1 row ************* a:1 **********2 row ************* a:2 |
臟讀發生條件是需要事務的隔離級別為 read uncommited;目前大部分資料庫至少設置為 read COMMITED;
6.5.2 不可重覆讀
不可重覆讀和臟讀的區別是:臟讀是讀到未提交的數據,而不可重覆讀讀到的是已提交的數據,但是違反了事務一致性的要求。
時間 | 會話A | 會話B |
1 |
et @@tx_isolation = 'read-commited' |
|
2 |
et @@tx_isolation = 'read-commited' |
|
3 | BEGIN | BEGIN |
4 |
select * from t ; **********1 row ************* a:1 |
|
5 | insert into t select 2; | |
6 | COMMITED | |
7 |
select * from t ; **********1 row ************* a:1 **********2 row ************* a:2 |
一般來說,不可重覆讀是可接受的,因為讀到的是已提交的數據,本身沒有帶來很大問題。在 InnoDB中使用 next-key lock避免不可重覆讀問題,即 幻讀(Phantom Problem)。在 Next-Key lock演算法下,對索引的掃描,不僅是鎖住掃描到的索引,還有這些索引覆蓋的範圍,因此在這個範圍內插入是不允許的。這樣則避免了另外的事務在這個範圍內的插入導致不可重讀的問題。
6.5.3 丟失更新
丟失更新就是一個事務的更新操作被另一個事務的更新操作覆蓋,從而導致數據不一致。
- 事務1將行記錄r更新為v1,但是事務未提交
- 事務2將行記錄r更新為v2,事務未提交
- 事務1提交
- 事務2提交
當前資料庫的任何隔離級別下,以上情況都不會導致資料庫理論意義上的丟失更新問題,因為,對於行的DML操作,需要對行貨其他粗粒度級別的對象加鎖,步驟2,事務2並不能對記錄進行更新,被阻塞,直到事務1提交。
但在生產應用中,還有一個邏輯意義的丟失更新問題,而導致該問題的不是因為資料庫本身的問題,簡單來說,下麵情況會發生丟失更新:
- 事務T1查詢一行數據,放入本地記憶體,返回給User1
- 事務T2查詢一行數據,放入本地記憶體,返回給User2
- User1修改後,更新資料庫提交
- User2修改後,更新資料庫提交
顯然,這個過程中,User1的修改操作”丟失“了。在銀行操作中,尤為恐怖。要避免丟失,需要讓事務串列化。
時間 | 會話A | 會話B |
1 | BEGIN | |
2 |
select cash into @cash from account where user=pUser for update #加X鎖 |
|
3 |
select cash into @cash from account where user=pUser for update #等待,直到m提交後,鎖釋放 |
|
…… | …… | |
m |
update account set cash = @cash - 9000 where user = pUser |
|
m+1 | commit | |
m+2 |
update account set cash = @cash -1 where user = pUser |
|
m+3 | commit |
6.6 阻塞
因為不同鎖之間的相容性關係,在有些時刻一個事務中的鎖需要等待另一個事務中的鎖釋放它所占用的資源,這就是阻塞。
阻塞並不是一件壞事,其是為了確保事務可以併發且正常地運行。
需要牢記的是,預設情況下,InnoDB存儲引擎不會回滾超時引發的錯誤異常,其實,InnoDB在大部分情況下都不會對異常進行回滾。
時間 | 會話A | 會話B |
1 |
select * from t; **********3 row ************* a:1 a:2 a:4 |
|
2 | BEGIN | |
3 |
select * from t where a < 4 for update; **********2 row ************* a:1 a:2 #對(2,4)上X鎖 |
|
4 | BEGIN | |
5 | insert into t select 5; | |
6 |
insert into t select 3; #等待超時,需要確定超時後,5的插入是否需要回滾或提交,否則這是十分危險的狀態 |
6.7 死鎖
死鎖是指兩個或兩個以上的事務在執行過程中,因爭奪資源而造成的一種相互等待的現象。解決死鎖的辦法:
- 超時回滾
- wait-for graph 等待圖的方式進行死鎖檢測,採用深度有限的演算法,選擇回滾undo量最小的事務。
死鎖的概率與以下因素有關:
- 事務的數量n,數量越多死鎖概率越大
- 事務操作的數量r,數量越多,死鎖概率越大
- 操作數據的集合R,越小,死鎖概率越大
6.8 鎖升級
鎖升級就是將當前鎖的粒度降低,例如把行鎖升級為頁鎖,把頁鎖升級為表鎖。
InnoDB不存在鎖升級,因為其不是根據每個記錄來產生行鎖的。相反,其根據每個事務訪問的每個頁對鎖進行管理,採用的是點陣圖的方式。因此不管一個事務鎖住頁中的一個記錄還是多個記錄,開銷都是一樣的。