mysql技術內幕(六)鎖

#鎖

6.1 什麼是鎖

  鎖是數據庫系統區別於文件系統的一個關鍵特性。鎖機制用於管理對共享資源的併發訪問飛InnoDB 存儲引擎會在行級別上對錶數據上鎖,這固然不錯。不過InnoDB 存儲引擎也會在數據庫內部其他多個地方使用鎖,從而允許對多種不同資源提供併發訪問。例如,操作緩衝池中的LRU 列表,刪除、添加、移動LRU 列表中的元素,爲了保證一致性,必須有鎖的介入。數據庫系統使用鎖是爲了支持對共享資源進行併發訪問,提供數據的完整性和一致性。
  另一點需要理解的是,雖然現在數據庫系統做得越來越類似,但是有多少種數據庫,就可能有多少種鎖的實現方法。在SQL 語法層面,因爲SQL 標準的存在,要熟悉多個關係數據庫系統並不是一件難事。
  InnoDB 存儲引擎鎖的實現和Oracle 數據庫非常類似,提供一致性的非鎖定讀、行級鎖支持。行級鎖沒有相關額外的開銷,並可以同時得到併發性和一致性

6.2 lock 與latch

  這裏還要區分鎖中容易令人混淆的概念lock 與latch。在數據庫中, lock 與latch 都可以被稱爲"鎖"。但是兩者有着截然不同的含義,本章主要關注的是lock。latch 一般稱爲問鎖(輕量級的鎖),因爲其要求鎖定的時間必須非常短。若持續的時間長,則應用的性能會非常差。在InnoDB 存儲引擎中, latch 又可以分爲mutex (互斥量〉和rwlock (讀寫鎖〉。其目的是用來保證併發線程操作臨界資源的正確性,並且通常沒有死鎖檢測的機制。lock 的對象是事務,用來鎖定的是數據庫中的對象,如表、頁、行。並且一般lock的對象僅在事務comrnit 或rollback 後進行釋放〈不同事務隔離級別釋放的時間可能不同〉。此外, lock, 正如在大多數數據庫中一樣, 是有死鎖機制的。
在這裏插入圖片描述

6.3 InnoDB 存儲引擎中的鎖

6.3.1 鎖的類型

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

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

  如果一個事務T1已經獲得了行r 的共享鎖,那麼另外的事務T2 可以立即獲得行r的共享鎖,因爲讀取並沒有改變行r 的數據,稱這種情況爲鎖兼容(Lock Compatible) 。但若有其他的事務T3 想獲得行r 的排他鎖,則其必須等待事務Tl、T2 釋放行r 上的共享鎖-一這種情況稱爲鎖不兼容。
在這裏插入圖片描述
  從表可以發現X 鎖與任何的鎖都不兼容,而S 鎖僅和S 鎖兼容。需要特別注意的是, S 和X 鎖都是行鎖,兼容是指對同一記錄(row) 鎖的兼容性情況。此外, InnoDB 存儲引擎支持多粒度(granular) 鎖定,這種鎖定允許事務在行級上的鎖和表級上的鎖同時存在。爲了支持在不同粒度上進行加鎖操作, InnoDB 存儲引擎支持一種額外的鎖方式,稱之爲意向鎖(Intention Lock) 。意向鎖是將鎖定的對象分爲多個層次,意向鎖意味着事務希望在更細位度〈fine granularity )上進行加鎖,如圖6-3 所示
在這裏插入圖片描述
   若將上鎖的對象看成一棵樹,那麼對最下層的對象上鎖,也就是對最細粒度的對象進行上鎖,那麼首先需要對粗粒度的對象上鎖。例如圖6-3 ,如果需要對頁上的記錄r 進行上X 鎖,那麼分別需要對數據庫A、表、頁上意向鎖 IX ,最後對記錄r 上X 鎖。若其中任何一個部分導致等待,那麼該操作需要等待粗粒度鎖的完成。舉例來說,在對記錄r 加X 鎖之前,已經有事務對錶l 進行了S 表鎖,那麼表1 上已存在S 鎖,之後事務需要對記錄r 在表l 上加上IX ,由於不兼容,所以該事務需要等待表鎖操作的完成。
  InnoDB 存儲引擎支持意向鎖設計比較簡練,其意向鎖即爲表級別的鎖。設計目的主要是爲了在一個事務中揭示下一行將被請求的鎖類型。其支持兩種意向鎖:
1 )意向共享鎖(lS Lock) ,事務想要獲得一張表中某幾行的共享鎖
2) 意向排他鎖(IX Lock) ,事務想要獲得一張表中某幾行的排他鎖
由於InnoDB 存儲引擎支持的是行級別的鎖,因此意向鎖其實不會阻塞除全表掃以外的任何請求。故表級意向鎖與行級鎖的兼容性如表6-4 所示。
在這裏插入圖片描述

6.3.2 一致性非鎖定讀

  一致性的非鎖定讀(consistent nonlocking read) 是指InnoDB 存儲引擎通過行多版本控制(multi versioning) 的方式來讀取當前執行時間數據庫中行的數據。如果讀取的行正在執行DELETE 或UPDATE 操作,這時讀取操作不會因此去等待行上鎖的釋放。相反地,InnoDB 存儲引擎會去讀取行的一個快照數據。之所以稱其爲非鎖定讀,因爲不需要等待訪問的行上X 鎖的釋放。
在這裏插入圖片描述
  快照數據是指該行的之前版本的數據,該實現是通過undo 段來完成。而undo用來在事務中回滾數據,因此快照數據本身是沒有額外的開銷。此外,讀取快照數據是不需要上鎖的,因爲沒有事務需要對歷史的數據進行修改操作。
   可以看到, 非鎖定讀機制極大地提高了數據庫的併發性。在InnoDB 存儲引擎的默認設置下,這是默認的讀取方式,即讀取不會佔用和等待表上的鎖。但是在不同事務隔離級別下,讀取的方式不同,並不是在每個事務隔離級別下都是採用非鎖定的一致性讀。此外,即使都是使用非鎖定的一致性讀,但是對於快照數據的定義也各不相同。
   ,快照數據其實就是當前行數據之前的歷史版本,每行記錄可能有多個版本。就圖6-4 所顯示的,一個行記錄可能有不止一個快照數據,一般稱這種技術爲行多版本技術。由此帶來的併發控制,稱之爲多版本併發控制(Multi Version Concurrency Control. MVCC) 。
在事務隔離級別READ COMMITTED 和REPEATABLE READ (InnoDB 存儲引擎的默認事務隔離級別〉下, InnoDB 存儲引擎使用非鎖定的一致性讀。然而,對於快照數據的定義卻不相同。在READ COMMITTED 事務隔離級別下,對於快照數據,非一致性讀總是讀取被鎖定行的最新一份快照數據。而在REPEATABLE READ 事務隔離級別下,對於快照數據,非一致性讀總是讀取事務開始時的行數據版本。

6.3 .3 一致性鎖定讀

在默認配置下,即事務的隔離級別爲REPEATABLE READ 模式下, InnoDB 存儲引擎的SELECT 操作使用一致性非鎖定讀。但是在某些情況下,用戶需要顯式地對數據庫讀取操作進行加鎖以保證數據邏輯的一致性。而這要求數據庫支持加鎖語句,即使是對千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…FOR UPDATE, SELECT…LOCK IN SHARE MODE 必須在一個事務中,當事務提交了,鎖也就釋放了。因此在使用上述兩句SELECT 鎖定語句時,務必加上BEGIN, START TRANSACTION 或者SET AUTOCOMMIT=0 。

6.3.4 自增長與鎖

  在InnoDB 存儲引擎的內存結構中,對每個含有自增長值的表都有一個自增長計數器(auto-increment counter) 。當對含有自增長的計數器的表進行插人操作時,這個計數器會被初始化。
  插入操作會依據這個自增長的計數器值加1 賦予自增長列。這個實現方式稱做AUTO-INC Locking。這種鎖其實是採用一種特殊的表鎖機制,爲了提高插入的性能,鎖不是在一個事務完成後才釋放,而是在完成對自增長值插入的SQL 語句後立即釋放。
  InnoDB 存儲引擎中提供了一種輕量級互斥量的自增長實現機制,這種機制大大提高了自增長值插入的性能。並且從該版本開始, InnoDB 存儲引擎提供了一個參數innodb_autoinc lock_ mode 來控制自增長的模式

6.3.5 外鍵和鎖

  前面已經介紹了外鍵,外鍵主要用於引用完整性的約束檢查。在InnoDB 存儲引擎中,對於一個外鍵列,如果沒有顯式地對這個列加索引, lnnoDB 存儲引擎自動對其加一個索引,因爲這樣可以避免表鎖。
  對於外鍵值的插入或更新,首先需要查詢父表中的記錄,即SELECT 父表。但是對於父表的SELECT 操作,不是使用一致性非鎖定讀的方式,因爲這樣會發生數據不一致的問題,因此這時使用的是SELECT…LOCK IN SHARE MODE 方式,即主動對父表加一個S 鎖。如果這時父表上已經這樣加X 鎖,子表上的操作會被阻塞。

6.4 鎖的算法

6.4.1 行鎖的3 種算法

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

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

  Record Lock 總是會去鎖住索引記錄,如果lnnoDB 存儲引擎表在建立的時候沒有設置任何一個索引,那麼這時InnoDB 存儲引擎會使用隱式的主鍵來進行鎖定。Next-Key Lock 是結合了Gap Lock 和Record Lock 的一種鎖定算法,在Next-Key Lock 算法下, InnoDB 對於行的查詢都是採用這種鎖定算法。
  例如一個索引有10, 11,13 和20 這四個值,那麼該索引可能被Next-Key Locking 的區間爲:
(-無窮,10]
(10, 11 ]
(11, 13]
(13, 20]
(20, +無窮)

若事務Tl 已經通過next-key locking 鎖定了如下範圍:
(10, 11) 、(11, 13)
當插入新的記錄12 時,則鎖定的範圍會變成:
( 10 , 11] 、(11, 12] 、( 12, 13]
然而,當查詢的索引含有唯一屬性時, InnoDB 存儲引擎會對Next-Key Lock 進行優化,將其降級爲Record Lock, 即僅鎖住索引本身,而不是範圍。
innodb會對輔助索引的下一個範圍加一個Gap lock,加入插入的記錄索引爲12 ,那麼會在(11, 12)上加Next-Key Lock ,在(12,13)加間隙鎖(就是把帶有12 的整個範圍都鎖上),Gap Lock 的作用是爲了阻止多個事務將記錄插入到同一範圍內。
   用戶可以通過以下兩種方式來顯式地關閉Gap Lock:

  • 將事務的隔離級別設置爲READ COMMITTED
  • 將參數innodb_locks_ unsafe for binlog 設置爲1
      在上述的配置下,除了外鍵約束和唯一性檢查依然需要的Gap Lock, 其餘情況僅使用Record Lock 進行鎖定。但需要牢記的是,上述設置破壞了事務的隔離性,並且對於replication, 可能會導致主從數據的不一致。此外,從性能上來看, READ COMMITTED也不會優於默認的事務隔離級別READ REPEATABLE 。

  對千唯一鍵值的鎖定, Next-Key Lock 降級爲Record Lock僅存在於查詢所有的唯一索引列。若唯一索引由多個列組成,而查詢僅是查找多個唯一索引列中的其中一個,那麼查詢其實是range 類型查詢,而不是point 類型查詢,故lnnoDB 存儲引擎依然使用Next-Key Lock 進行鎖定。

6.4.2 解決Phantom Problem

  在默認的事務隔離級別下,即REPEATABLE READ 下, InnoDB 存儲引擎採用Next-Key Locking 機制來避免Phantom Problem (幻像問題)
  Phantom Problem 是指在同一事務下,連續執行兩次同樣的SOL 語句可能導致不同的結果,第二次的SOL 語句可能會返回之前不存在的行。InnoDB 存儲引擎默認的事務隔離級別是REPEATABLE READ, 在該隔離級別下,其採用Next-Key Locking 的方式來加鎖。而在事務隔離級別READ COMMITTED 下,其僅採用Record Lock。

6.5 鎖問題

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

6.5.1 髒讀

   髒頁指的是在緩衝池中已經被修改的頁,但是還沒有刷新到磁盤中,即數據庫實例內存中的頁和磁盤中的頁的數據是不一致的,當然在刷新到磁盤之前,日誌都已經被寫入到了重做日誌文件中。而所謂髒數據是指事務對緩衝池中行記錄的修改,並且還沒有被提交(commit) 。
   對於髒頁的讀取,是非常正常的。髒頁是因爲數據庫實例內存和磁盤的異步造成的,這並不影響數據的一致性(或者說兩者最終會達到一致性,即當髒頁都刷回到磁盤)。並且因爲髒頁的刷新是異步的,不影響數據庫的可用性,因此可以帶來性能的提高。髒數據卻截然不同,髒數據是指未提交的數據,如果讀到了髒數據,即一個事務可以讀到另外一個事務中未提交的數據,則顯然違反了數據庫的隔離性。
   髒讀指的就是在不同的事務下,當前事務可以讀到另外事務未提交的數據,簡單來說就是可以讀到髒數據。
  髒讀發生的條件是需要事務的隔離級別爲READ UNCOMMITTED

6.5.2 不可重複讀

  不可重複讀是指在一個事務內多次讀取同一數據集合。在這個事務還沒有結束時,另外一個事務也訪問該同一數據集合,並做了一些DML 操作。因此,在第一個事務中的兩次讀數據之間,由於第二個事務的修改,那麼第一個事務兩次讀到的數據可能是不一樣的。這樣就發生了在一個事務內兩次讀到的數據是不一樣的情況,這種情況稱爲不可重複讀。
  不可重複讀和髒讀的區別是:髒讀是讀到未提交的數據,而不可重複讀讀到的卻是已經提交的數據,但是其違反了數據庫事務一致性的要求。可以通過下面一個例子來觀察不可重複讀的情況。
就是在一個事物沒有結束的時候,查詢了兩次。如果另一個事物在此期間插入數據並提交事物,那麼A事物兩次查詢的結果就不一致了。
  隔離級別是READ COMMITTED,可通過Next-Key Lock 算法來避免不可重複讀的問題。

6.5.3 丟失更新

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

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

6.6 阻塞

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

6.7 死鎖

6.7.1 死鎖的概念

   死鎖是指兩個或兩個以上的事務在執行過程中,因爭奪鎖資源而造成的一種互相等待的現象。若無外力作用, 事務都將無法推進下去。解決死鎖問題最簡單的方式是不要有等待,將任何的等待都轉化爲回滾,並且事務重新開始。毫無疑問,這的確可以避免死鎖問題的產生。然而在線上環境中,這可能導致併發性能的下降,甚至任何一個事務都不能進行。而這所帶來的問題遠比死鎖問題更爲嚴重,因爲這很難被發現並且浪費資源。
   解決死鎖問題最簡單的一種方法是超時,即當兩個事務互相等待時,當一個等待時間超過設置的某一闕值時,其中一個事務進行回滾,另一個等待的事務就能繼續進行。在InnoDB 存儲引擎中,參數innodb_lock_ wait_ timeout 用來設置超時的時間。
   ,除了超時機制,當前數據庫還都普遍採用wait-for graph (等待圖)的方式來進行死鎖檢測。較之超時的解決方案,這是一種更爲主動的死鎖檢測方式。InnoDB 存儲引擎也採用的這種方式。

6.8 鎖升級

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

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