讀書筆記: 數據庫與MySql(3)

本文將主要介紹MySql數據庫的鎖機制,內容主要出自《MySql性能調優與架構優化》。


MySql數據庫鎖定機制

爲了保證數據的一致性,任何一個數據庫都存在鎖定機制。鎖定機制的優劣直接影響到一個數據庫系統的併發處理能力和性能,所以鎖定機制的實現也就成爲了各種數據庫的核心技術之一。

MySql鎖定機制簡介

數據庫鎖定機制簡單來說就是數據庫爲了保證數據的一致性而使各種共享資源在被併發訪問不發生錯誤所設計的一種規則。

MySql數據庫由於其自身架構的特點,存在多種數據存儲引擎,每種存儲引擎所針對的應用場景特點都不太一樣,爲了滿足各自特定應用場景的需求,每種存儲引擎的鎖定機制都是爲各自所面對的特定場景而優化設計的,所以各引擎的鎖定機制也有較大區別。

總的來說,MySql各存儲引擎使用了三種類型(級別)的鎖定機制:行級鎖定,頁級鎖定和表級鎖定。

行級鎖定(row-level)

行級鎖定的粒度最小,也是目前各大數據庫軟件所實現的鎖定粒度最小的。由於鎖定粒度小,發生鎖爭用的概率也小,能夠給予應用程序儘可能大的併發處理能力。有優點自然有缺點:由於鎖定資源的粒度很小,所以每次獲取鎖和釋放鎖需要做的事情也更多,帶來的消耗更大。此外,也最容易發生死鎖。

表級鎖定(table-level)

表級鎖定是MySql各存儲引擎中最大粒度的鎖定機制。特點是實現邏輯非常簡單,帶來的系統消耗也最小,加鎖和釋放鎖速度很快。由於表級鎖定一次鎖定整個表,所以可以避免死鎖。但是鎖定粒度大會導致鎖定資源爭用的概率高,影響數據庫的併發處理能力。

頁級鎖定(page-level)

頁級鎖定是MySql中比較獨特的一種鎖定級別,在其他數據庫管理軟件中也不是太常見。鎖定粒度介於行級鎖定和表級鎖定之間,相應地,併發處理能力也在兩者之間,也會發生死鎖。

總體來說,鎖的粒度越小,鎖定相同數量的數據需要的資源越多,實現的算法也會越來越複雜。不過隨着鎖粒度的減小,應用程序的訪問請求遇到鎖等待的可能性也隨之降低,系統的整體併發度也隨之提升。

在MySql中,使用表級鎖定的主要是MyISAM,Memory和CSV等一些非事務性存儲引擎,使用行級鎖定的主要是Innodb和NDB Cluster存儲引擎,使用頁級鎖定的主要是BerkeleyDb存儲引擎。

鎖定機制分析

MySql最初的設想是,提供一種獨立於各種引擎之外的鎖定機制,其初始設計就是表鎖定機制。當然,後來隨着各種不同引擎的接入,發現獨立的鎖定機制是行不通的,於是就開放給各個引擎自己來實現了。

表級鎖定

表級鎖定主要分爲兩種類型,一種是讀鎖定,一種是寫鎖定。在MySql中,主要通過四個列隊來維護這兩種鎖定,兩個存放當前正在鎖定中的讀和寫鎖定信息,另外兩個存放等待中的讀和寫鎖定信息:

  • Current read-lock queue (lock->read)
  • Pending read-lock queue (lock->read_wait)
  • Current write-lock queue (lock->write)
  • Pending write-lock queue (lock->write_wait)

當前持有讀鎖的所有線程的相關信息都能夠在Current read-lock queue中找到,隊列中的信息按照獲取到鎖的時間依序存放。而正在等待鎖定資源的信息則存放在Pending read-lock queue 裏面,另外兩個存放寫鎖信息的隊列也按照上面相同規則來存放信息。

一個新的客戶端請求在申請獲取讀鎖定資源的時候,需要滿足以下條件:
(1)請求鎖定的資源當前沒有被寫鎖定;
(2)寫鎖定等待隊列中沒有更高優先級的寫鎖定等待;
如果滿足了上面兩個條件之後,該請求會被立即通過,並將相關的信息存入Current read-lock queue中,不滿足就會被迫進入讀鎖定等待隊列。

一個新的客戶端請求在申請獲取寫鎖定資源的時候,需要滿足以下條件:
(1)請求鎖定的資源既沒有被寫鎖定,也沒有被讀鎖定;
(2)請求鎖定的資源不在寫鎖定等待隊列中。
當滿足這兩個條件時,請求會被立即通過,相關信息將存入Current write-lock queue中,否則進入寫鎖定等待隊列。

事實上,mysql內部有許多其他的鎖定方式,並不僅僅只有單純的共享讀鎖定和互斥寫鎖定。在某些特殊的鎖定方式下,申請獲取寫鎖定也會被立即通過。這是因爲mysql內部知道該如何處理,而不產生併發錯誤。

MyISAM引擎使用的鎖定機制完全是MySql提供的表級鎖定實現。

行級鎖定

行級鎖定不是MySql自己實現的鎖定方式,而是由其他存儲引擎自己實現的,如Innodb存儲引擎,以及MySql分佈式存儲引擎NDB Cluster等都實現了行級鎖定。

Innodb是目前事務性存儲引擎中使用最爲廣泛的存儲引擎,所以這裏主要分析一下Innodb的鎖定特性。

總的來說,Innodb的鎖定機制和Oracle數據庫有不少相似之處。Innodb的行級鎖定同樣分爲兩種類型:共享鎖和排它鎖。在鎖定機制的實現過程中,爲了讓行級鎖定和表級鎖定共存,Innodb也同樣使用了意向鎖的概念,也就有了意向共享鎖和意向排它鎖兩種。

共享鎖(S)和排他鎖(X)也叫讀共享鎖和寫互斥鎖,讀與讀之間是共享的,其他均是互斥的。當一個事務以加鎖的方式讀一行時,另一個事務不能對這一行進行更改。當然,因爲在很多情況下,MySql中的讀都是通過MVCC來實現的快照讀,不需要加鎖,所以讀的同時並不影響寫操作。具體可參考博主另一篇介紹隔離性的文章的最後一節。

意向鎖
意向鎖是表級別的鎖(只是表級別鎖,但不是表鎖,注意概念),意向鎖是在行鎖的基礎上產生的,表示事務有意對錶中的某些行加鎖。
意向鎖分爲意向共享鎖(IS)和意向排他鎖(IX)。事務要獲得某些行的共享鎖(S鎖)之前,必須先獲得表的IS鎖,要獲得排他鎖(X鎖)之前,必須先獲得表的IX鎖。

在MySql官網上有這樣一張表:

共享鎖(S) 排他鎖(X) 意向共享鎖(IS) 意向排他鎖(IX)
共享鎖(S) 兼容 衝突 兼容 衝突
排他鎖(X) 衝突 衝突 衝突 衝突
意向共享鎖(IS) 兼容 衝突 兼容 兼容
意向排他鎖(IX) 衝突 衝突 兼容 兼容

S鎖和X鎖之間的共存關係很好理解,只有讀-讀是可以共存的。而意向鎖與行鎖之間的共存關係怎麼理解呢?如排他鎖X與意向排他鎖IX之間的互斥關係。下面舉例說明:

假設有兩個事務A和B,A通過唯一索引列來更新一行數據,那麼需要在更新的行上加排他鎖,而由於在加排他鎖之前系統會自動加上意向排他鎖,因此A更新這行數據時會產生IX鎖和X鎖,此時B事務通過唯一索引來更新另一行數據,同理會產生IX鎖和X鎖。如果按表中的共存關係,是不是說B事務在加IX鎖時會阻塞呢?並不是這樣。表中IX和X互斥,針對的是同一行。意向鎖是在行鎖的基礎上產生的,與行鎖有很強的關聯關係。如果事務B的IX鎖是針對事務A鎖住的同一行而產生的(也就是說B後續的X鎖是爲了跟A鎖住同一行),這種情況下才是互斥的,否則是沒有問題的。

這與我們直覺中不同事務可以同時更新表中不同的行,但不能同時更新同一行是相符合的。爲了理解起來方便,可以直接忽略掉意向鎖,分析S鎖和X鎖即可。

意向鎖的作用
從併發的角度來看,行鎖似乎已經滿足了讀和寫的併發操作要求。那麼爲什麼還需要意向鎖呢?意向鎖是爲了解決表鎖和行鎖並存的問題而出現的。

注意上面的栗子中,兩個事務更新數據行時,是根據唯一索引列來檢索的。事實上,在MySql中,行鎖是加在索引上的,如果沒有索引,就不會使用行鎖,而是使用表鎖。假如在事務A和事務B根據索引來更新不同行的時候,此時又有一個事務C來更新另外一行,無索引情況下加表鎖。那麼InnoDB怎麼判斷表鎖是否可以加鎖成功?很簡單,通過意向鎖判斷即可。事務A和事務B對數據行的更新會產生兩個IX鎖,事務C發現表上已經有了IX鎖,說明已經或者即將有X鎖來對錶中的行加鎖,於是表寫鎖就被阻塞,不能成功加鎖。如果沒有意向鎖,那就需要一行一行的檢查,看錶中是否存在行鎖,然後才能判斷表鎖能不能加鎖成功,這樣的話就比較耗費時間和性能了。

行鎖的實現方式
與oracle中行鎖直接鎖定物理記錄不同,Innodb的鎖定是通過在指向數據記錄的第一個索引鍵之前和最後一個索引鍵之後的空域空間上標記鎖定信息而實現的,這種鎖定實現方式被稱爲“NEXT-KEY locking”(間隙鎖)。

在Query執行過程中通過範圍查找的話,會鎖定整個範圍內所有的索引鍵值,即使這個鍵值並不存在。間隙鎖有個致命的弱點,就是當鎖定一個範圍鍵值之後,即使某些不存在的鍵值也會被無辜的鎖定,而造成在鎖定的時候無法插入鎖定鍵值範圍內的任何數據。在某些場景下這可能會對性能造成很大的危害。而Innodb給出的解釋是爲了阻止幻讀的出現,所以選擇用間隙鎖來實現鎖定。

通過索引實現鎖定的方式還存在其他幾個較大的性能隱患:

  • 當Query無法利用索引的時候,Innodb會放棄使用行級鎖定而改用表級鎖定,造成併發性能的降低。
  • 當Query使用的索引並不包含所有過濾條件時,數據檢索使用的索引鍵所執行的數據可能有部分並不屬於該Query的結果集的行列,但是也會被鎖定。如 select … from table where id < 10 and name = ‘test’ lock in share mode,雖然最終結果集可能只有一條數據,但卻會鎖住id<10這個範圍的全部索引。
  • 當Query在使用索引定位數據的時候,如果使用的索引鍵一樣但訪問的數據行不同的時候(索引只是過濾條件的一部分),一樣會被鎖定。

Innodb實現了未提交讀,提交讀,可重複讀和序列化四種事務隔離級別。同時爲了保證數據在事務中的一致性,實現了多版本數據訪問,具體可參考博主另一篇介紹隔離性的文章。

在Innodb的事務管理和鎖定機制中,有專門的檢測死鎖的機制,會在系統中產生死鎖之後的很短時間內就檢測到該死鎖的存在。當Innodb檢測到系統中產生了死鎖之後,Innodb會通過相應的判斷來選擇產生死鎖的兩個事務中較小的事務來回滾,而讓另一個較大的事務成功完成。在Innodb發現死鎖之後,會計算兩個事務各自插入、更新或者刪除的數據量來判斷兩個事務的大小。也就是說哪個事務所改變的記錄條數越多,在死鎖中就越不會被回滾掉。但是有一點需要注意,當產生死鎖的場景中涉及到不止Innodb存儲引擎的時候,Innodb是沒辦法檢測到該死鎖的,這時候就只能通過鎖定超時限制來解決死鎖了。

鎖優化與檢測

Innodb 行鎖優化建議

Innodb存儲引擎由於實現了行級鎖定,雖然在鎖定機制方面所帶來的性能損耗可能比表級鎖定要高一些,但是在整體併發處理能力方面要遠遠優於MyISAM的表級鎖定的。當系統的併發量較高的時候,Innodb的整體性能與MyISAM相比就會有比較明顯的優勢了。但是Innodb的行級鎖定也有其脆弱的一面,當使用不當的時候可能會導致Innodb性能大大降低。

要想合理利用Innodb的行級鎖定,需要做好以下工作:

a) 儘可能讓所有的數據檢索都通過索引來完成,從而避免Innodb因爲無法通過索引鍵加鎖而升級爲表級鎖定。

b) 合理設計索引,讓Innodb在索引鍵上面加鎖的時候儘可能準確,儘可能縮小鎖定範圍,避免造成不必要的鎖定而影響其他query的執行。

c) 儘可能減少基於範圍的數據檢索過濾條件,避免因爲間隙帶來的負面影響而鎖定了不該鎖定的記錄。

d) 儘量控制事務的大小,減少鎖定的資源量和鎖定時間長度。

e) 在業務環境允許的情況下,儘量使用較低界別的事務隔離,以減少MySql因爲事先事務隔離級別而帶來的附加成本;

針對死鎖,有以下建議:

a) 類似業務模塊中,儘可能按照相同的訪問順序來訪問,防止產生死鎖。

b) 在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖產生效率。

c) 對於非常容易產生死鎖的業務部分,可以嘗試使用升級鎖定粒度,通過表級鎖定來減少死鎖產生的概率。

系統鎖定爭用情況查詢

對於兩種鎖定級別,MySQL 內部有兩組專門的狀態變量記錄系統內部鎖資源爭用情況,我們先看看MySQL 實現的表級鎖定的爭用狀態變量:

mysql> show status like 'table%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 100 |
| Table_locks_waited | 0 |
+-----------------------+-------+

這裏有兩個狀態變量記錄MySql內部表級鎖定的情況,兩個變量說明如下:

  • Table_locks_immediate:產生表級鎖定的次數;
  • Table_locks_waited:出現表級鎖定爭用而發生等待的次數。

兩個狀態值都是從系統啓動後開始記錄,每出現一次對應的事件則數量加1。如果這裏的Table_locks_waited 狀態值比較高,那麼說明系統中表級鎖定爭用現象比較嚴重,就需要進一步分析爲什麼會有較多的鎖定資源爭用了。

對於Innodb 所使用的行級鎖定,系統中是通過另外一組更爲詳細的狀態變量來記錄的,如下:

mysql> show status like 'innodb_row_lock%';
+-------------------------------+--------+
| Variable_name | Value |
+-------------------------------+--------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 599255 |
| Innodb_row_lock_time_avg | 213 |
| Innodb_row_lock_time_max | 6878 |
| Innodb_row_lock_waits | 2810 |

Innodb 的行級鎖定狀態變量不僅記錄了鎖定等待次數,還記錄了鎖定總時長,每次平均時長,以及最大時長,此外還有一個非累積狀態量顯示了當前正在等待鎖定的等待數量。

此外,Innodb 除了提供這五個系統狀態變量之外,還提供的其他更爲豐富的即時狀態信息供我們分析使用。可以通過如下方法查看:

  • 通過創建Innodb Monitor 表來打開Innodb 的monitor 功能:
    mysql> create table innodb_monitor(a int) engine=innodb;
  • 然後通過使用“SHOW INNODB STATUS”查看細節信息;

爲什麼要先創建一個叫innodb_monitor 的表呢?因爲創建該表實際上就是告訴Innodb 我們開始要監控他的細節狀態了,然後Innodb 就會將比較詳細的事務以及鎖定信息記錄進入MySQL 的error log 中,以便我們後面做進一步分析使用。

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