MySQL鎖機制漫談(一)

前言

數據庫鎖定機制是數據庫為了保證數據的一致性而使各種共享資源在并發訪問時變的有序的一種規則。MySQL數據庫的各種存儲引擎使用了三種的鎖定機制:行級鎖定、頁級鎖定、表級鎖定。

  • 行級鎖定
    行級鎖定最大的特點就是鎖定對象的顆粒度很小,也是目前各大數據庫管理軟件所實現的鎖定粒度最小的。由于鎖定粒度很小,所以發生鎖定資源爭用的概率也最小,能夠給予應用程序盡可能大的并發處理能力,從而提高一些需要高并發應用系統的整體性能。雖然能夠在并發處理能力上面有較大的優勢,但是行級鎖定也因此帶來了不少弊端。由于鎖定資源的顆粒度很小,所以每次獲取鎖和釋放鎖需要做的事情也更多,帶來的消耗自然也就更大了。此外,行級鎖定也最容易發生死鎖。

  • 表級鎖定
    和行級鎖定相反,表級別的鎖定是MySQL 各存儲引擎中最大粒度的鎖定機制。該鎖定機制最大的特點是實現邏輯非常簡單,帶來的系統負面影響最小。所以獲取鎖和釋放鎖的速度很快。由于表級鎖一次會將整個表定,所以可以很好的避免困擾我們的死鎖問題。當然,鎖定顆粒度大所帶來最大的負面影響就是出現鎖定資源爭用的概率也會最高,致使并大度大打折扣。

  • 頁級鎖定
    頁級鎖定是MySQL 中比較獨特的一種鎖定級別,在其他數據庫管理軟件中也并不是太常見。頁級鎖定的特點是鎖定顆粒度介于行級鎖定與表級鎖之間,所以獲取鎖定所需要的資源開銷,以及所能提供的并發處理能力也同樣是介于上面二者之間。另外,頁級鎖定和行級鎖定一樣,會發生死鎖。

在MySQL 數據庫中,使用表級鎖定的主要是MyISAM,Memory,CSV 等一些非事務性存儲引擎,而使用行級鎖定的主要是Innodb 存儲引擎和NDB Cluster 存儲引擎,頁級鎖定主要是BerkeleyDB 存儲引擎的鎖定方式。

下面,我們主要針對MySQL中的表級鎖定和行級鎖定進行具體的實現原理分析。

鎖定機制分析

表級鎖定

MySQL本身就提供表級鎖定支持。MySQL的表級鎖定主要分為兩種:一種是讀鎖定,一種是寫鎖定。在MySQL中,主要通過4個隊列來維護這兩種鎖定:兩個存放當前正在鎖定的讀和寫鎖定信息;另外兩個存放等待中的讀寫鎖定信息。這四個隊列信息如下:

  • 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 里面,剩下的寫鎖隊列也按照上面相同規則來存放信息。

讀鎖定

一個新的客戶端線程在請求獲得表的讀鎖定時需要滿足連個條件:

  • 請求鎖定的資源當前沒有被寫鎖定
  • 寫鎖定等待隊列(Pending write-lock queue)中沒有更高優先級的寫鎖定等待

如果滿足了上面兩個條件之后,該請求會被立即通過,并將相關的信息存入Current read-lockqueue 中,而如果上面兩個條件中任何一個沒有滿足,都會被迫進入等待隊列Pending read-lock queue中等待資源的釋放。

寫鎖定

行級鎖定

MySQL本身沒有實現行級鎖定,InnoDB是目前事務型存儲引擎中使用最為廣泛的,我們下面以InnoDB為例來分析行級鎖的實現機制。

概念

InnoDB在鎖定機制的實現過程中為了讓行級鎖定和表級鎖定共存, Innodb使用了意向鎖(表級鎖定)的概念,也就有了意向共享鎖和意向排他鎖這兩種。
Innodb 的行級鎖定同樣分為兩種類型,共享鎖和排他鎖:

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

如果一個事務T1已經獲得了行r的共享所鎖,那么事務T2是可以立即獲得行r上的共享鎖,因為讀取行為并不會該百年行r的數據,這種情況稱為鎖兼容。但如果事務T3想要獲得行r的排它鎖,則必須等待事務T1、T2先釋放行r上的共享鎖-這種情況稱為鎖不兼容。
注:S Lock與X Lock都是行鎖,兼容是指對同一記錄鎖的兼容性情況。
同時,由于InnoDB存儲引擎支持多粒度的鎖定,這種鎖定允許事務在行級上的鎖和表級上的鎖同時存在。為支持在不同粒度上的加鎖行為,InnoDB支持另外一種加鎖方式,即所謂的意向鎖(Intention Lock)。意向鎖是將鎖定的對象分為多個層次,意向鎖是的事務可以在更細的粒度上進行加鎖操作。
意向鎖怎么使用呢,很簡單:

SELECT ...... IN SHARE MODE;//設置一個Intention Share Lock
SELECT ......FOR UPDATE;//設置一個Intention Xtra Lock

意向鎖的作用就是當一個事務在需要獲取資源鎖定的時候,如果遇到自己需要的資源已經被排他鎖占用的時候,該事務可以需要鎖定行的表上面添加一個合適的意向鎖。如果自己需要一個共享鎖,那么就在表上面添加一個意向共享鎖。而如果自己需要的是某行(或者某些行)上面添加一個排他鎖的話,則先在表上面添加一個意向排他鎖。意向共享鎖可以同時并存多個,但是意向排他鎖同時只能有一個存在。所以我們可以總結出Innodb 的鎖定模式實際上可以分為四種:共享鎖(S),排他鎖(X),意向共享鎖(IS)和意向排他鎖(IX),它們的兼容關系如下:

IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

InnoDB存儲引擎對意向鎖的設計比較簡單,它的意向鎖就是表級別的鎖。
但是到這里,我們還是沒能明白意向鎖存在的意義,看了一下mysql開發者文檔,原文如下:

Thus, intention locks do not block anything except full table requests (for example, LOCK TABLES ... WRITE). The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.
即意向鎖只會阻塞全表鎖的請求。IX與IS的主要目的在于告訴你,這張表里有行鎖存在或有意向對某行加鎖。

那意向鎖是要解決一個什么問題呢?表鎖與行鎖共存的問題??紤]這樣一種情況,事務A對row1加了一個讀鎖,其他事務對該行只能讀不能寫。然后事務B要對整個表加一個寫鎖。此時事務B首先需要判斷該表是否可以加表鎖,這個步驟就是:

  • step1,判斷表是否被其他事務加上了表鎖。
  • step2,判斷表中每一行是否都有行鎖。
    我們需要注意的就是step2,每一行的去檢查效率太低了,這會需要遍歷整個表。此時意向鎖可以派上用場了。在意向鎖存在的情況下,事務A想要對某一行加行鎖,必須先申請表的意向鎖,成功后再申請加行鎖。
    此時,以上的表鎖與行鎖共存問題步驟會編程如下:
  • step1同上。
  • step2,先判斷該表上是否有意向鎖,如果存在意向鎖,說明表中可能某行加了行鎖,因此事務B的寫鎖會被直接阻塞,而不需要再次掃表驗證。效率大大提高。

行鎖算法

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

  • Record Lock

單個行記錄上的鎖。Record Lock總是會去鎖住索引記錄,如果InnoDB存儲引擎在表建立的時候沒有設置任何一個索引,那么此時InnoDB存儲引擎會創建一個隱式的聚集索引并使用該索引記錄進行鎖定。在RC隔離級別下一般加的都是該類型的記錄鎖(但唯一二級索引上的duplicate key檢查除外,總是加LOCK_ORDINARY類型的鎖)

  • Gap Lock

間隙鎖,鎖定一個范圍,但不包含記錄本身。間隙鎖是通過在指向數據記錄的第一個索引鍵之前和最后一個索引鍵之后的空域空間上標記鎖定信息而實現的。稱之為間隙鎖是因為Query 執行過程中通過過范圍查找的,他會鎖定整個范圍內所有的索引鍵值,即使這個鍵值并不存在。在使用唯一索引搜索唯一記錄時不需要顯示聲明間隙鎖。一般在RR隔離級別下會使用到GAP鎖。InnoDB中的間隙鎖時‘完全非排他的’-這意味著InnoDB中的間隙鎖僅僅會阻止其他事物在間隙中插入記錄,但并不會阻止不同事物在相同的間隙上加鎖。
間隙鎖可以顯示的關閉。你可以通過切換到RC隔離級別,或者開啟選項innodb_locks_unsafe_for_binlog來避免GAP鎖。這時候只有在檢查外鍵約束或者duplicate key檢查時才會使用到GAP LOCK。間隙鎖是性能與并發下的折中方案。

  • Next-Key Lock

Gap Lock+Record Lock,在索引記錄上加記錄鎖以及索引記錄前的間隙上加間隙鎖。在Next-Key Lock算法下,如果一個會話在記錄R上獲得了一個共享鎖或者排他鎖,則此時另一個會話就不能以索引的順序在該索引前的間隙上插入記錄。假設一個索引有值 10, 11, 13, 20,則該索引上會覆蓋的next-key鎖會包含一下區間。對于最后一個區間,next-key lock會鎖住從索引中最大的記錄到無窮大的區間。

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

默認情況下,MySQL在RR隔離級別下使用 next-lock算法進行查詢,索引掃描。該算法可以解決在RR隔離級別下出現的幻讀問題。所謂幻讀就是一個事務內執行相同的查詢,會看到不同的行記錄。在RR隔離級別下這是不允許的。
注:當查詢所有唯一索引列時,InnoDB存儲引擎會對Next-Key Lock進行優化,會將鎖降級為Record Lock,即僅鎖住索引本身,而非一個范圍。
我們通過一個例子對 Next-Key Lock算法進行驗證說明:

首先我們先創建一個表t;

CREATE TABLE t(a INT,b INT,PRIMARY KEY (a),KEY (b))engine=innodb CHARSET=utf8;
INSERT INTO t SELECT 1,1;
INSERT INTO t SELECT 3,1;
INSERT INTO t SELECT 5,3;
INSERT INTO t SELECT 7,6;
INSERT INTO t SELECT 10,8;

如果事務T1執行該SQL語句:

SELECT * FROM t WHERE b=3 FOR UPDATE;

該語句通過索引列b進行查詢,因此會使用傳統的Next-Key Lock技術加鎖。同時由于表中存在兩個索引,需分別進行鎖定。對于聚集索引,僅對列a等于5的索引加上Record Lock。而對于輔助索引b,加上的是Next-Key Lock,鎖定范圍(1,3).同時需要注意,InnoDB存儲引擎還會對輔助索引的下一個鍵值加上Gap Lock,即還有一個(3,6)的鎖。此時其他事務插入任何b列值在(1,6)都是會被阻塞。

  • Insert intention locks

插入意向鎖是一種由INSERT操作設置的間隙鎖。該鎖表明了執行插入操作的意向,因此多個事務在相同的索引間隙中執行插入操作,并且也不是在同一個位置上插入,則事務之間不需要互相等待。
舉個例子,兩個事物分別嘗試插入值5,6。在獲取插入行的排他鎖之前,兩個事務都會獲?。?,7]之間的插入意向鎖。由于不存在沖突,它們不會互相阻塞。

  • AUTO-INC Locks

AUTO-INC鎖是一個特殊的表級鎖,當一個事務嘗試在帶有AUTO_INCREMENT列的表上進行插入操作,該鎖會被獲取。在最簡單的情況下,一個事務正在向標中插入數據,任何其他想要進行插入操作的事務必須等待,只有這樣第一個插入行的事務才能得到連續的primary key值。
innodb_autoinc_lock_mode 配置項可以控制AUTO-INC Locks算法的開啟。

幻讀問題

我們先看一下Phantom Problem的定義:

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

在InnoDB存儲引擎中,對于INSERT操作,會檢查插入的下一個記錄是否被鎖定,若以鎖定,則無法插入。
InnoDB采用Next-Key Lock算法來避免Phantom Problem。InnoDB存儲引擎默認事務隔離級別是REPEATABLE READ,在該隔離級別下采用Next-Key Lock來加鎖。在事務隔離級別READ COMMITED下,采用Record Lock。

Problem

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

  • 當Query 無法利用索引的時候,Innodb 會放棄使用行級別鎖定而改用表級別的鎖定,造成并發性能的降低;
  • 當Quuery 使用的索引并不包含所有過濾條件的時候,數據檢索使用到的索引鍵所只想的數據可能有部分并不屬于該Query 的結果集的行列,但是也會被鎖定,因為間隙鎖鎖定的是一個范圍,而不是具體的索引鍵;
  • 當Query 在使用索引定位數據的時候,如果使用的索引鍵一樣但訪問的數據行不同的時候(索引只是過濾條件的一部分),一樣會被鎖定

InnoDB行鎖優化建議

Innodb 存儲引擎由于實現了行級鎖定,雖然在鎖定機制的實現方面所帶來的性能損耗可能比表級鎖定會要更高一些,但是在整體并發處理能力方面要遠遠優于MyISAM 的表級鎖定的。當系統并發量較高的時候Innodb 的整體性能和MyISAM 相比就會有比較明顯的優勢了。但是,Innodb 的行級鎖定同樣也有其脆弱的一面,當我們使用不當的時候,可能會讓Innodb 的整體性能表現不僅不能比MyISAM 高,甚至可能會更差。因此我們有如下建議:

  • 盡可能讓所有的數據檢索都通過索引來完成,從而避免Innodb 因為無法通過索引鍵加鎖而升級為表級鎖定;
  • 合理設計索引,讓Innodb 在索引鍵上面加鎖的時候盡可能準確,盡可能的縮小鎖定范圍,避免造成不必要的鎖定而影響其他Query 的執行;
  • 盡可能減少基于范圍的數據檢索過濾條件,避免因為間隙鎖帶來的負面影響而鎖定了不該鎖定的記錄
  • 盡量控制事務的大小,減少鎖定的資源量和鎖定時間長度;
  • 在業務環境允許的情況下,盡量使用較低級別的事務隔離,以減少MySQL 因為實現事務隔離級別所帶來的附加成本;

參考

  • MySQL性能調優與架構設計
  • MySQL技術內幕
  • mysql dev manual
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容