【MySQL】(六)鎖

開發多用戶、數據庫驅動的應用時,最大的一個難點是:一方面要最大程度地利用數據庫的併發訪問,另一方面還要確保每個用戶能以一致的方式讀取和修改數據。爲此就有了鎖(locking)的機制,同時這也是數據庫系統區別於文件系統的一個關鍵特性。本篇文章將詳細介紹InnoDB存儲引擎對錶中數據的鎖定,同時分析InnoDB存儲引擎會以怎樣的粒度鎖定數據。

1、什麼是鎖

鎖是數據庫系統區別於文件系統的一個關鍵特性。鎖機制用於管理對共享資源的併發訪問。InnoDB存儲引擎會在行級別上對錶數據上鎖。不過InnoDB存儲引擎也會在數據庫內部其他多個地方使用鎖,從而允許對多種不同資源提供併發訪問。

對於MyISAM存儲引擎,其鎖是表鎖設計。對於Microsoft SQL Server 2005版本之前其都是頁鎖的,相對於表鎖的MyISAM引擎來說,併發性能有所提高。到2005版本,Microsoft SQL Server開始支持樂觀併發和悲觀併發,在樂觀併發下支持行級鎖,但是其實現方式與InnoDB存儲引擎的實現方式完全不同。

InnoDB存儲引擎鎖的實現和Oracle數據庫非常相似,提供一致性的非鎖定讀、行級鎖支持。行級鎖沒有相關額外的開銷,並可以同時得到併發性和一致性。

2、lock與latch

在數據庫中,lock與latch都可以被稱爲“鎖”。但是兩者有着截然不同的含義,本篇文章主要關注的是lock。

latch一般稱爲閂鎖(輕量級的鎖),因爲其要求鎖定的時間必須非常短。若持續的時間長,則應用的性能會非常差。在InnoDB存儲引擎中,latch又可以分爲mutex(互斥量)和rwlock(讀寫鎖)。其目的是爲了保證併發線程操作臨界資源的正確性,並且通常沒有死鎖檢測的機制。

lock的對象時事務,用來鎖定的是數據庫中的對象,如表、頁、行。並且一般lock的對象僅在事務commit或rollback後進行釋放(不同事務隔離級別釋放的時間可能不同)。此外,正如在大多數數據庫中一樣,是有死鎖機制的。

3、InnoDB存儲引擎中的鎖

3.1、鎖的類型

InnoDB存儲引擎實現瞭如下兩種標準的行級鎖:

  • 共享鎖(S Lock),允許事務讀一行數據。
  • 排他鎖(X Lock),允許事務刪除或更新一行數據。

如果一個事務T1已經獲得了行r的共享鎖,那麼另外的事務T2可以立即獲得行r的共享鎖,因爲讀取並沒有改變行r的數據,成這種情況爲鎖兼容。但若有其他的事務T3想獲得行r的排他鎖,則其必須等待事務T1、T2釋放行r上的共享鎖——這種情況稱爲不兼容。
這裏寫圖片描述

此外,InnoDB存儲引擎支持多粒度鎖定,這種鎖定允許事務在行級上的鎖和表級上的鎖同時存在。爲了支持在不同粒度上進行加鎖操作,InnoDB存儲引擎支持一種額外的鎖方式,稱之爲意向鎖。意向鎖是將鎖定的對象分爲多個層次,意向鎖意味着事務希望在更細粒度上進行加鎖,如下圖
這裏寫圖片描述
若將上鎖的對象看成一棵樹,那麼對最下層的對象上鎖,也就是對最細粒度的對象進行上鎖,那麼首先需要對粗粒度的對象上鎖。例如,如果需要對頁上的記錄r進行上X鎖,那麼分別需要對數據庫A、表、頁上意向鎖IX,最後對記錄r上X鎖。若其中任何一個部分導致等待,那麼該操作需要等待粗粒度鎖的完成。

InnoDB存儲引擎支持意向鎖設計比較簡練,其意向鎖即爲表級別的鎖。設計目的主要是爲了在一個事務中揭示下一行將別請求的鎖類型。其支持兩種意向鎖:

  1. 意向共享鎖(IS Lock),事務想要獲得一張表中某幾行的共享鎖
  2. 意向排他鎖(IX Lock),事務想要獲得一張表中某幾行的排他鎖

由於InnoDB支持的行級別的鎖,因此意向鎖其實不會阻塞除全表掃以以外的任何請求。故表級別意向鎖與行級別意向鎖的兼容性如下表所示
這裏寫圖片描述

3.2、一致性非鎖定讀

一致性的非鎖定讀是指在InnoDB存儲引擎通過行多版本控制的方式來讀取當前執行時間數據庫中行的數據。如果讀取的行正在執行DELETE或UPDATE操作,這時讀取操作不會因此去等待行上鎖的釋放。相反地,InnoDB存儲引擎會去讀取行的一個快照數據。

之所以稱其爲非鎖定讀。因爲不需要等待訪問的行上X鎖的釋放。快照數據是指該行的之前版本的數據,該實現是通過undo段來完成。而undo用來在事務中回滾數據,因此快照數據本身是沒有額外的開銷。此外,讀取快照數據時不需要上鎖的,因爲沒有事務需要對歷史的數據進行修改操作。

快照數據其實就是當前行數據的歷史版本,沒行記錄可能由多個版本。一個行記錄可能有不止一個快照數據,一般稱這種技術爲行多版本技術。由此帶來的併發控制,稱之爲多版本併發控制。

在事務隔離級別READ COMMITED和REPRTABLE READ下,InnoDB存儲引擎使用非鎖定一致性讀。在READ COMMITED事務隔離級別下,對於快照數據,非一致性讀總是讀取被鎖定行的最新一份快照數據。而在REPEATABLE READ事務隔離級別下,對於快照數據,非一致性讀總是讀取事務開始時的行數據版本。

3.3、一致性鎖定讀

在默認配置下,即事務的隔離級別爲REPEATABLE READ模式下,InnoDB存儲引擎的SELECT操作使用一致性非鎖定讀。 但是在某些情況下,用戶需要顯示地對數據庫讀取操作進行加鎖以保證數據邏輯的一致性。InnoDB存儲引擎對於SELECT語句支持兩種一致性鎖定讀操作:

  • SELECT…FOR UPDATE
  • SELECT…LOCK IN SHARE MODE
    SELECT…FOR UPDATE對讀取的行記錄加一個X鎖,其他事務不能對已鎖定的行加上任何鎖。SELECT…LOCK IN SHARE MODE對讀取的行記錄加一個S鎖,其他事務可以向被鎖定的行加S鎖,但是如果加X鎖,則會別阻塞。

對於一致性非鎖定讀,及時讀取的行已被執行了SELECT…FOR UPDATE,也是可以進行讀取的,這和上面講的一樣。此外,SELECT…LOCK IN SHARE MODE必須在一個事務中,當事務提交了,鎖也就是釋放了。

3.4、自增長與鎖

在InnoDB存儲引擎的內存結構中,對每個含有自增長值的表都有一個自增長計數器。當含有自增長的計數器的表進行插入操作時,這個計數器會被初始化,執行如下的語句來得到計數器的值:

SELECT MAX(auto_inc_col) FROM t FOR UPDATE;

插入操作會依據這個自增長的計數器值加1賦予自增長列。這個實現方式稱作AUTO-INC Locking。這種鎖其實是一種特殊的表鎖機制,爲了提高插入的性能,鎖不是在一個事務完成後才釋放,而是在完成對自增長值的插入的SQL語句後立即釋放。

雖然AUTO-Locking 從一定程度上提高了併發插入效率,但還是存在一些性能上的問題。首先,對於有自增長值的列的併發插入性能較差,事務必須等待前一個插入的完成(雖然不用等待事務的完成)。其次,對於INSERT…SELECT的大量數據的插入回影響插入性能。因爲一個事務中的插入會被阻塞。

InnoDB存儲引擎中提供了一種輕量級互斥量的自增長實現機制,這種機制大大提高了自增長值插入的性能。

3.5、外鍵和鎖

外鍵主要用於引用完整性的約束檢查。在InnoDB存儲引擎中,對於一個外鍵列,如果沒有顯示地對這個列加索引,InnoDB存儲引擎會自動對其加一個索引,因爲這樣可以避免表鎖。

對於外鍵值的插入或更新,首先需要查詢父表中的記錄,即SELECT父表。但是對於父表的SELECT操作,不是使用一致性非鎖定讀的方式,因爲這樣會發生數據不一致的問題,因此這時使用的是SELECT…LOCK IN SHARE MODE方式,即主動對父表加一個S鎖。如果這時父表已經加X鎖,子表上的操作會被阻塞。

4、鎖的算法

4.1、行鎖的3種算法

INnoDB存儲引擎有3中行鎖的算法,其分別是:

  • Record Lock:單個行記錄上的鎖
  • Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄本身
  • Next-Key Lock:Gap Lock+Record Lock,鎖定一個範圍,並且鎖定記錄本身

Record Lock總是會去鎖住索引記錄,如果InnoDB存儲引擎表在建立的時候沒有設置任何一個索引,那麼這時InnoDB存儲引擎會使用隱式的主鍵來進行鎖定。

Next-Key Lock是結合了Gap Lock和Record Lock的一種鎖定算法,在Next-Key Lock算法下,InnoDB對於行的查詢都是採用這種鎖定算法。

4.2、解決Phantom Problem

在默認的事務隔離級別下,即REPEATABLE下,InnoDB存儲存儲引擎採用Next-Key Locking機制來避免Phantom problem(幻想問題)。

Phantom Problem是指在同一個事務下,連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能會返回之前不存在的行。

InnoDB存儲引擎採用Next-Key Locking的算法避免Phantom Problem。其鎖住的不是單個值,而是一個範圍,因此任何對於這個範圍的插入都是不被允許的,從而避免Phantom Problem。

5、鎖問題

通過鎖定機制可以實現事務的隔離性要求,使得事務可以併發地工作。鎖提高了併發,但是卻會帶來潛在的問題。不過好在因爲事務隔離性的要求,鎖只會帶來三種問題,如果可以防止這三種問題,那將不會產生併發異常。

5.1、髒讀

在理解髒讀之前,需要理解髒數據的概念。但是髒數據和之前所介紹的髒頁是兩種完全不同的概念。髒頁指的是在緩衝池中已經別修改的頁,但是還沒有刷新到磁盤中,即數據庫實例內存中的頁和磁盤中的頁的數據時不一致的,當然在刷新到磁盤之前,日誌都已經被寫入到了重做日誌文件中。而所謂髒數據是指事務對緩衝池中行記錄的修改,並且還沒有被提交(commit)。

對於髒頁的讀取,是非常正常的。髒頁是因爲數據庫實例內存和磁盤的異步造成的,這並不影響數據的一致性(或者說兩者最終會達到一致性,即當髒頁都刷新回到磁盤)。並且因爲髒頁的刷新是異步的,不影響數據庫的可用性,因此可以帶來性能的提高。

髒數據卻截然不同,髒數據是指未提交的數據,如果讀到了髒數據,即一個事務可以讀到另外一個事務中未提交的數據,則顯然違反了數據庫的隔離性。

髒讀指的就是在不同的事務下,當前事務可以讀到另外事務未提交的數據,簡單來說就是可以讀到髒數據。

髒讀顯現在生產環境中並不常發生,髒讀發生的條件是需要事務的隔離級別爲READ UNCOMMITED,而目前絕大部分數據庫都至少設置成READ COMMIT。

5.2、不可重複讀

不可重複讀是指在一個事務內多次讀取同一數據集合。在這個事務還沒有結束時,另外一個事務也訪問該同一數據集合,並做了一些DML操作。因此,在第一個事務中的兩次讀數據之間,由於第二個事務的修改,那麼第一個事務兩次讀到的數據可能是不一樣的。這樣就發生了在一個是事務內兩次讀到的數據時不一樣的情況,這種情況稱爲不可重複讀。

不可重複讀和髒讀的區別是:髒讀是讀到未提交的數據,而不可重複讀讀到的卻是已經提交的數據,但是違反了數據庫一致性的要求。

一般來說,不可重複讀的問題是可以接受的,因爲其讀到的是已經提交的數據,本身不會帶來很大的問題。因此,很多數據庫廠商將其數據庫事務的默認隔離級別設置爲READ COMMIT,在這種隔離級別下允許不可重複讀的現象。

在InnoDB存儲引擎中,通過使用Next-Key Lock算法來避免不可重複讀的問題。在MySQL官方文檔中將不可重複讀的問題定義爲Phantom Problem,即幻象問題。在Next-Key Lock算法下,對於索引的掃描,不僅是鎖住掃描到的索引,而且還鎖住這些索引覆蓋的範圍。因此在這個範圍內的插入都是不允許的。這樣就避免了另外的事務在這個範圍內插入數據導致的不可重複讀問題。因此,InnoDB存儲引擎的默認事務隔離級別是READ REPEATABLE,採用Next-Key Lock算法,避免了不可重複讀的現象。

5.3、丟失更新

丟失更新時另一個鎖導致的問題,簡單來說就是一個事務的更新操作會被另一個事務的更新操作鎖覆蓋,從而導致數據的不一致。例如:

  1. 事務T1將行記錄r更新爲v1,但是事務T1並未提交。
  2. 與此同時,事務T2將行記錄r更新爲v2,事務T2未提交。
  3. 事務T1提交
  4. 事務T2提交

但是,在當前數據庫的任何隔離級別下,都不會導致數據庫理論意義上的丟失更新問題。這是因爲,及時READUNCOMMIT的事務隔離級別,對於行的DML操作,需要對行或其他粗粒度級別的對象加鎖。因此在上述步驟2中,事務T2並不能對行記錄r進行更新操作,其會被阻塞,知道事務T1提交。

雖然數據庫能阻止丟失更新問題的產生,但是生產應用中還有另一個邏輯意義的丟失更新問題,而導致該問題的並不是因爲數據庫本身的問題。實際上,在所有多用戶計算機系統環境下都有可能產生這個問題。簡單來說,出現下面的情況時,就會發生丟失更新:

  1. 事務T1查詢一行數據,放入本地內存,並顯示給一個終端用戶User1
  2. 事務T2也查詢該行數據,並將取得的數據顯示給終端用戶User2
  3. User1修改這行記錄,更新數據庫並提交
  4. User2修改這行記錄,更新數據庫並提交

顯然,這個過程用戶User1的修改更新操作“丟失”了,而這可能導致一個“恐怖”的結果。

要避免丟失更新發生,需要讓食物在這種情況下的操作變成串行化,而不是並行的操作。即在上述四個步驟的1)中,對用戶讀取的記錄加上一個排他X鎖。同樣,在步驟2)的操作過程中,用戶同樣也需要加一個排他X鎖。通過這種方式,步驟2)就必須等待步驟1)和步驟3)完成,最後完成步驟4)。

6、阻塞

因爲不同鎖之前的兼容性關係,在有些時刻一個事務中的鎖需要等待另一個事務中的鎖釋放它所佔用的起源,這就是阻塞。阻塞並不是一件壞事,其是爲了確保事務可以併發且正常地運行。

7、死鎖

7.1、死鎖的概念

死鎖是指兩個或兩個以上的事務在執行過程中,因爭奪資源而造成的的一種互相等待的現象。解決死鎖問題最簡單的方式是不要有等待,將任何等待都化爲回滾,並且事務重新開始。這的確可以避免死鎖問題的產生。然而在線上生產環境中,這可能導致併發性能的下降,甚至任何一個事務都不能進行。

解決死鎖最簡單的一種方法是超時,即當兩個事務互相等待時,當一個等待時間超過設置得某一閾值時,其中一個事務進行回滾,另一個等待的事務就能繼續進行。

超時機制僅通過超時後對事務進行回滾的方式來處理,或者說起是根據FIFO的順序選擇回滾對象。

除了超時機制,當前數據庫還都普遍採用wait-for graph(等待圖)的方式來進行死鎖檢測。較之超時的解決方案,這是一種更爲主動的死鎖檢測方式。InnoDB存儲引擎頁採用的這種方式。wait-for graph要去數據庫保存一下兩種信息:

  • 鎖的信息鏈表
  • 事務等待鏈表

通過上述鏈表可以構造出一張圖,而在這個圖中若存在迴路,就代表死鎖。因此資源間相互發生等待。在wait-for graph 中,事務爲圖中的節點。而在圖中,事務T1指向T2邊的定義爲:

  • 事務T1等待事務T2所佔用的資源
  • 事務T1最終等待T2所佔用的資源,也就是事物之間在等待相同的資源,而事務T1發生在T2的後面

wait-for graph是一種較爲主動的死鎖檢測機制,在每個事務請求鎖併發生等待時都會判斷是否存在迴路,若存在則有死鎖,通常來說InnoDB存儲引擎選擇回滾undo量最小的事務。

8、鎖升級

鎖升級是指將當前鎖的粒度降低。舉例來說,數據庫可以把一個表1000個行鎖升級爲一個頁鎖,或者將頁鎖升級爲表鎖。如果在數據庫的設計中認爲鎖是一種稀有資源,而且像避免鎖的開銷,那麼數據庫中會頻繁出現鎖升級現象。

9、小結

儘管鎖本身相當直接,但是它的一些副作用卻不是這樣的。關鍵是用戶需要理解鎖帶來的問題,如丟失更新、髒讀、不可重複讀等。如果不知道這一點,那麼開發的應用程序性能就會很差。如果不學會怎樣通過一些命名和數據字典來查看事務鎖住了哪些資源,你可能永遠不知道到底發生了什麼事情,可能只是認爲MySQL數據庫有時會阻塞而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章