在mysql中,鎖機制看起來很複雜,一堆名詞:排他鎖、共享鎖、表鎖、間隙鎖、意向鎖等等,搞的初學者雲裏霧裏。同時鎖的相關知識又跟事務隔離級別、索引等概念有千絲萬縷的關係,是面試中的常規問題。mysql
上面的腦圖是對mysql鎖相關知識的一個梳理,但願可以幫到你們,讓你們可以:面試
按鎖的應用場景來看,分爲讀鎖和寫鎖,讀鎖又可稱爲S鎖和共享鎖;寫鎖又可稱爲X鎖和排他鎖。簡單來講,讀鎖 = S鎖 = 共享鎖,一樣,寫鎖 = X鎖 = 排他鎖。
正如他們的取名,只要碰到排他鎖,那麼就會阻塞,具體阻塞狀況以下表:sql
讀鎖 | 寫鎖 | |
---|---|---|
讀鎖 | 否 | 是 |
寫鎖 | 是 | 是 |
讀鎖和寫鎖是互斥的,讀寫操做是串行數據庫
咱們使用Mysql通常是使用InnoDB存儲引擎的。InnoDB和MyISAM有兩個本質的區別:併發
對於行鎖來講,也分2種類型的鎖mvc
其實事務隔離級別就是經過鎖機制來實現的,只不過隱藏了加鎖的細節,下面來看看二者的關係。post
你們都知道,innodb的事務隔離級別有4種性能
髒讀就是一個事務讀取到另外一個事務未提交的數據。出現髒讀的本質就是由於操做(修改)完該數據就立馬釋放掉鎖,致使讀的數據就變成了無用的或者是錯誤的數據。spa
避免髒讀的作法很簡單:就是把釋放鎖的位置調整到事務提交以後,此時在事務提交前,其餘進程是沒法對該行數據進行讀取的。即讀寫是串行的。
但Read committed會出現不可重複讀,即一個事務讀取到另一個事務已經提交的數據,也就是說一個事務能夠看到其餘事務所作的修改。屢次查詢數據庫的結果都不同。code
和不可重複讀相似,但虛讀(幻讀)會讀到其餘事務的插入的數據,致使先後讀取不一致。能夠把不可重複讀理解爲數據更新,幻讀是數據插入。
innodb經過MVCC解決了不可重複讀的問題,MVCC的具體原理下面介紹。同時結合間隙鎖,避免了幻讀。即innodb的Repeatable read其實不會出現幻讀的問題,innodb的事務默認級別就是Repeatable read
那麼MVCC到底是一種什麼機制,可以解決不可重複讀的問題?
MVCC即多版本併發控制,能夠簡單認爲 是行級鎖的一個升級版。前面提到,只有讀-讀場景是不阻塞的,其餘只有要寫(排他鎖)場景,都是阻塞的,必定程度上影響了讀寫效率。基於提高併發性能的考慮,MVCC通常讀寫是不阻塞的,因此說MVCC不少狀況下避免了加鎖的操做。
InnoDB中的MVCC,是經過在每行記錄後面保存兩個隱藏的列來實現的。這兩個列,一個保存了行的建立時間,一個保存行的刪除時間。固然存儲的並非實際的時間值,而是系統版本號。沒開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會做爲此事務的版本號,用來和查詢到的每行記錄的版本號進行比較。
舉個select的例子,InnoDB會根據如下兩個條件檢查每行記錄:
簡單總結下,多版本併發控制(MVCC)是一種用來解決讀-寫衝突的無鎖併發控制,也就是爲事務分配單向增加的時間戳,爲每一個修改保存一個版本,版本與事務時間戳關聯,讀操做只讀該事務開始前的數據庫的快照。 這樣在讀操做不用阻塞寫操做,寫操做不用阻塞讀操做的同時,避免了髒讀和不可重複讀。
MVCC雖然解決了不可重複讀問題,可是沒法解決幻讀,須要配合間隙鎖。
首先咱們看個例子,初始表以下:
id | x | y | 建立時間 | 刪除時間 |
---|---|---|---|---|
1 | 30 | 10 | 1 | undefined |
很簡單,一個自增id,一列x,一列y,假設有個限制條件:x+y <= 100。而後兩個事務同時併發執行:
T2和T3提交後,x+y=50+60=110 不符合小於100的要求。
Update的本質是 read --> write,MySQL(innodb)爲了解決這個問題,強行把 read 分紅了 snapshot read(快照讀)和 locking read (當前讀)。在 UPDATE 或者 SELECT ... FOR UPDATE 的時候,innodb 引擎實際執行的是當前讀。
在一個支持MVCC的併發系統中, 咱們須要支持兩種讀, 一個是快照讀, 一個是當前讀。
快照讀:簡單的select操做,屬於快照讀,不加鎖。
當前讀:特殊的讀操做,插入/更新/刪除操做,屬於當前讀,須要加鎖, 讀取的是最新數據。
給一個幻讀的例子:
update user set col1='new_val' where id=1; 結果: Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0
begin; insert into user values('A'); commit;
update user set col1='new_val' where id=1; 結果: Query OK, 1 rows affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
問題的本質是由於 update語句的查找階段至關於select ... for update,這會更新事務A的 ReadView,從而能夠讀到「其餘事務已提交的修改」。即出現了幻讀
把上面的例子用事務版本號來解釋:
id | col1 | 建立時間 | 刪除時間 |
---|---|---|---|
1 | A | 2 | undefined |
id | col1 | 建立時間 | 刪除時間 |
---|---|---|---|
1 | A | 2 | 1 |
1 | A | 1 | undefined |
InnoDB 經過加間隙鎖的方式,解決幻讀。innodb對於鍵值在條件範圍內但並不存在的記錄(叫作『間隙』)加鎖,這種鎖機制就是所謂的間隙鎖。 相對的,能夠把上面不一樣的行鎖稱爲記錄鎖。
間隙鎖產生的條件分惟一索引和普通索引:
id
BETWEEN 5 AND 7 FOR UPDATE具體實驗例子能夠參考 MySQL的鎖機制 - 記錄鎖、間隙鎖、臨鍵鎖,很是詳細。
針對以上幻讀的例子,update語句select * from user where id=1 for update
,id是惟一索引,可是因爲id=1的記錄不存在,因而產生了間隙鎖(排他),能夠阻塞其餘事務的insert操做。
MySQL(innodb)的選擇是容許在快照讀以後執行當前讀,而且更新 snapshot 鏡像的版本。嚴格來講,這個結果違反了 repeatable read 隔離級別,,可是 who cares 呢,畢竟官方都說了:「This is not a bug but an intended and documented behavior.」
表鎖相對來講就很簡單了,表鎖顧名思義,就是鎖針對的範圍是整張表。表鎖開銷小,加鎖快,不會出現死鎖;鎖定力度大,發生鎖衝突機率高,併發度最低。如今考慮這樣一個情景:
事務A獲取了某一行的排他鎖,並未提交:
SELECT * FROM users WHERE id = 6 FOR UPDATE;
事務B想要獲取users表的表鎖:
LOCK TABLES users READ;
由於共享鎖與排他鎖互斥,因此事務B得確保:
爲了檢測是否知足第二個條件,事務 B 必須在確保 users表不存在任何排他鎖的前提下,去檢測表中的每一行是否存在排他鎖。掃描全部行這明顯是一個效率不好的作法,因而提出了意向鎖。
事務在獲取行鎖(包括讀鎖和寫鎖)的同時會獲取表的意向鎖(包括讀鎖和寫鎖)。
意向鎖之間是相互兼容的:
意向共享鎖 | 意向排他鎖 | |
---|---|---|
意向共享鎖 | 兼容 | 兼容 |
意向排他鎖 | 兼容 | 兼容 |
意向鎖和表級鎖之間存在互斥狀況
意向共享鎖 | 意向排他鎖 | |
---|---|---|
表級共享鎖 | 兼容 | 互斥 |
表級排他鎖 | 互斥 | 互斥 |
這裏再次強調下:
這裏的排他 / 共享鎖指的都是表鎖!!!意向鎖不會與行級的共享 / 排他鎖互斥!!!
意向鎖不會與行級的共享 / 排他鎖互斥!!!
如今再回過頭來看剛纔的例子:
事務A獲取了某一行的排他鎖,並未提交:
SELECT * FROM users WHERE id = 6 FOR UPDATE;
此時,事務A也獲取了users表的意向排他鎖,
事務B想要獲取users表的表鎖:
LOCK TABLES users READ;
發現此表存在乎向排他鎖,因而事務B被阻塞,直到意向排他鎖被釋放。