MySQL中的事務原理和鎖機制

本文主要總結 MySQL 事務幾種隔離級別的實現和其中鎖的使用狀況。html

在開始前先簡單回顧事務幾種隔離級別以及帶來的問題。mysql

四種隔離級別:讀未提交、讀已提交、可重複讀、可串行化。sql

帶來的問題:髒讀、不可重複讀、幻讀。分別是由讀未提交、讀已提交、可重複讀引發的。數據庫

髒讀:一個事務讀取到在另外一個事務還未提交時的修改。多線程

不可重複讀:一個事務在另外一個事務提交先後讀取到了不一樣數據。(側重於某一條數據,這條數據內容發生了變化)。併發

幻讀:一個事務在另外一個事務提交先後讀取到了不一樣數據。(側重於多了或是少了一條數據)。高併發

在 Mysql 中,默認隔離級別是可重複讀,在默認時卻必定程度上解決了幻讀,爲何這麼說呢?請看下面這個例子。測試

同時咱們查看數據庫中的數據:spa

 能夠看到並無發生 「幻讀」,這是爲何?難道可重複讀級別已經解決了「幻讀」?後面會詳細解釋。.net

 

Mysql 中的鎖

對於存儲引擎 MyISAM ,只支持表級鎖,對於 InnoDB 來講,既支持表級鎖、也支持行級鎖。因此 InnoDB 能夠用於高併發的場景下而 MyISAM 不行。

按顆粒度劃分

一、行級鎖

只對一行數據加鎖,當一個事務操做某一行事務時,只對該行數據加排他鎖時,其餘事務對其餘行數據操做時不會影響,併發性好。缺點是在加多條數據時加鎖會比較耗時。

使用場景:可串行化隔離級別

二、表級鎖

對整張表進行加鎖。加鎖快可是可承受的併發量低。

三、頁級鎖

對一頁數據進行加鎖,介於行級鎖與表級鎖之間。

 

按種類劃分

一、共享鎖(讀鎖)

共享鎖是對於MySQL中的讀操做的,因此共享鎖也叫讀鎖,一個事務進行讀操做時,會對讀取的數據添加讀鎖(可串行化下的讀操做是自動加鎖的,其餘隔離級別須要在查詢語句後面添加 lock in share mode),加鎖後其餘事務也能夠對加鎖的數據進行讀取。

 

二、排他鎖(寫鎖)

排它鎖是對於 MySQL 中的寫操做的,因此排它鎖也叫寫鎖。添加排它鎖的數據其餘事務就不能進行操做,同時共享鎖與排它鎖也是互斥的,也就是一個事務對某數據添加了共享鎖,那麼其餘事務就不能對其再添加排它鎖。在全部隔離級別級別中的修改操做(insert、update、delete)都會添加排他鎖,而讀操做能夠經過在語句後面添加 for update 來對讀取的數據添加排它鎖

 

其餘種類

一、Record Lock

記錄鎖。record lock 是加在具體記錄對應聚簇索引上的鎖,它是鎖住的是索引自己而不是記錄,若是該表沒有聚簇索引,也會建立一個聚簇索引來代替。換句話說 record lock 屬於行級鎖。它既能夠是共享鎖也能夠是排它鎖(到底是共享鎖仍是排他鎖上面已經分析了)。任何級別都會存在。

二、Gap Lock

間隙鎖,就是加在兩條數據索引之間的鎖,好比數據表student(id,name),id 是主鍵,有數據(5,"aa"),(7,"bb"),隔離級別是可串行化。此時事務1執行select * from student where id>5 and id<7,那麼就會對 (4,7) 添加間隙鎖,鎖住中間的間隙。好比說事務2執行insert into(6,"cc"),那麼次操做就會被阻塞。在可重複讀及以上級別纔會有。

三、Next-Key Lock

指的是 Record Lock 與 Gap Lock 的結合。針對 Gap Lock 中的例子,若是事務1執行的是 select * from dept where id>4 and id<8,那麼對數據(5,"aa")、(7,"bb")對應的聚簇索引上也會添加 Record Lock。同時(4,5),(5,7),(7,8)也會加上間隙鎖。同 Gap Lock 同樣,只有可重複讀以以上級別纔會出現。

 

 

四種隔離級別的實現

在說明原理前,先了解一下什麼是快照讀和當前讀。

快照讀Mysql 默認的隔離級別是「可重複讀」。經過文章開頭的例子能夠看出左邊事務在右邊事務執行修改提交先後查詢的數據都同樣,左邊事務的查詢就是一個快照讀。快照讀的數據能夠看做一個快照,其餘事務的修改不會改變這個快照值。也就是說快照讀的數據不必定是最新值,可重複讀級別也所以才保證了 「可重複讀」。快照讀的優點是不用加鎖,併發效率高。

使用場景:在 Mysql 的隔離級別中,除了可串行化級別的讀外,其餘隔離級別中事務的讀都是快照讀。

 

當前讀當前讀指的就是讀的是最新值。既然是要求是最新值,那麼就須要進行加鎖限制,因此當前讀是須要加鎖的,同時由於當前讀必定是最新的數據,因此就沒法保證 「可重複讀」。

使用場景:首先是可串行化中事務的讀操做是當前讀,而四種隔離級別中的全部修改(insert、update、delete)操做都屬於當前讀。可能你以爲讀操做和修改操做沒有關係,可是事實是這些修改操做是先 「讀」 找到數據具體的位置才能進行 「修改」。

 

讀已提交和可重複讀的實現

這兩種隔離級別的實現歸功於 MVCC 機制。

MVCC機制

MVCC機制也叫多版本併發控制,用於控制數據庫的併發訪問。在 Mysql 的 InnoDB 存儲引擎中主要做用於實現讀已提交和可重複讀隔離級別。實現原理是經過 undo日誌版本鏈和 Read View 。

一、undo日誌版本鏈。在 InnoDB 聚簇索引記錄的行數據中有兩個隱藏列,trx_id 和 roll_pointer,trx_id 表示當前行數據上次被修改的事務 id (事務 ID 是自增的,越新的事務 ID 越大),roll_pointer 是每次在修改完數據前,都會將修改前的數據存入undo log(專門用於記錄事務修改前數據的日誌系統,用於進行事務的回滾和生成數據快照),roll_pointer 就是當前行數據修改前在 undo 日誌中的存儲位置。

二、Read View。內部主要有四個部分組成,第一個是建立 Read View 的事務 id creator_trx_id,第二個是建立 Read View 時還未提交的事務 id 集合trx_ids,第三個是未提交事務 id 集合中的最大值up_limit_id,第四個是未提交事務 id 集合中的最小值low_limit_id。

當執行查詢操做時會先找磁盤上的數據,而後根據 Read View 裏的各個值進行判斷,

1)若是該數據的 trx_id 等於 creator_trx_id,那麼就說明這條數據是建立 Read View的事務修改的,那麼就直接返回;

2)若是大於 up_limit_id,說明是新事務修改的,那麼會根據 roll_pointer 找到上一個版本的數據從新比較;

3)若是小於 low_limit_id,那麼說明是以前的事務修改的數據,那麼就直接返回;

4)若是是在 low_limit_id 與 up_limit_id 中間,那麼須要去 trx_ids 中逐個查找,若是存在,就根據 roll_pointer找打上一個版本的數據,而後再判斷;若是不存在就說明該數據是建立 Read View 時就已經修改好的了,能夠返回。

 

而讀已提交和可重複讀之因此不一樣就是它們 Read View 生成機制不一樣,讀已提交是每次 select 都會從新生成一次,而可重複讀是一次事務只會在第一次查詢時生成一個 Read View。

舉個借鑑於網上的例子,好比事務1先修改 name 爲小明1,假設此時事務id 是60,那麼就會在修改前將以前的50寫入 undo log,同時在修改時將生成的undo log 行數據地址寫入 roll_pointer,而後暫不提交事務1。開一個事務2,事務 id 爲 55,進行查詢操做,此時生成的 Read View 的trx_ids是[60],creator_trx_id 爲 55,對應的數據狀態就是下圖,首先先獲得磁盤數據的 trx_id ,爲60,而後判斷,不等於 creator_trx_id,而後檢查,最大值和最小值都是 60,因此經過 roll_pointer 從 undo log 中找到 「小明」 那條數據,再次判斷,發現 50 是小於 60的,因此知足,返回數據。

而後提交事務1,再開一個事務3,將name改爲小明2,假設此時的事務 id 是100,那麼在修改前又會將 trx_id 爲 60 拷貝進 undo log,同時修改時將 trx_id 改成100,而後事務3暫不提交,此時事務1再進行select。若是隔離級別是讀已提交,那麼就會從新生成 Read View,trx_ids是[100],creator_trx_id 爲55,判斷過程和上面類似,最終返回的是小明1那條數據;而若是是可重複讀,那麼仍是一開始的 Read View,trx_ids 仍是[60],creator_trx_id 仍是 55,那麼仍是從小明2 的 trx_id 進行判斷,發現不等於 55,且大於60,跳到 小明1 ,對 trx_id判斷,仍是大於,最終仍是返回 「小明」 那條數據。下面是這個例子最終的示意圖

 

 

讀未提交和可串行化實現

這兩個實現比較簡單。讀未提交就是每次事務執行的修改都更新到對應的數據上,而後讀取直接讀取這個數據就能夠了。而可串行化則是使用了讀鎖和寫鎖以及間隙鎖來實現的,對會形成「幻讀」、「髒讀」、「不可重複讀」 的操做會進行阻塞,也正由於這樣,極易任意形成阻塞,因此不建議使用可串行化級別。

 

 

不一樣隔離級別下加鎖狀況

對於不一樣的隔離級別,不一樣的列狀況,加鎖狀況都各不不一樣,下面會列舉各個場景下加鎖的狀況。

一、讀未提交級別

讀操做不會加鎖,寫操做會添加排它鎖。由於會發生髒讀,因此 MVCC並不會發生效果。能夠手動添加 for update 、lock in share mode 來加鎖,不會產生間隙鎖,只有記錄鎖。

不管是否使用索引,是不是手動添加鎖,只會對最終操做的數據加 Record Lock。

 

二、讀已提交級別

讀操做不會加鎖,寫操做會添加排它鎖。MVCC 會在每次查詢時生成 Read View,能夠手動添加 for update 、lock in share mode 來加鎖,不會產生間隙鎖,只有記錄鎖。

不管是否使用索引,是不是手動添加鎖,只會對最終操做的數據加 Record Lock。(在未使用到索引時數據庫會對全部數據加鎖,當加載到 Server 層篩選後會將不符合條件的數據進行解鎖,因此咱們會認爲只對最終操做的數據加鎖,讀未提交級別的未使用索引狀況也相同)

 

三、可重複讀級別

可重複讀是一個特殊的隔離級別,爲何這麼說呢?由於它是 mysql 默認的隔離級別,由於 "可串行化" 級別默認對讀操做加鎖,致使程序的併發性不高,因此不建議使用,而可重複讀由於使用的是快照讀,因此併發性很好,而且解決了不可重複讀、髒讀以及 "快照讀" 幻讀,但同時會有 "當前讀"幻讀的問題產生(下面"MySQL 對幻讀的解決" 會詳細解釋),因此針對這個問題引入了間隙鎖來解決。

讀操做不會加鎖,寫操做會添加排它鎖。MVCC 會在事務開始第一次查詢時生成 Read View,能夠手動添加 for update、lock in share mode 來加鎖,可能會產生間隙鎖。

不管是不是手動添加鎖,1)在使用到惟一索引和主鍵索引時,會對對應記錄對應的聚簇索引上添加 Record Lock。

2)在使用非惟一索引時,會對對應數據左右的間隙額外添加間隙鎖,也就是使用 Next-key Lock。如下圖爲例

 name 爲主鍵,id 爲普通索引。當執行 delete from t1 where id = 10 時,因爲新增的數據可能在 [ (6,c),(10,b) ] 之間,[ (10,b),(10,d) ] 之間,[ (10,d),(11,f) ] 之間,因此須要對這三個間隙加鎖,來防止在事務1操做時其餘事務對這三個位置進行其餘修改操做致使操做出錯。好比如今 insert (10,a),那麼就會斷定是 [ (6,c),(10,b) ] 之間的,此操做就會被阻塞。

3)若是沒有用到索引,那麼會對全部數據以及他們兩邊的間隙進行加 Next-key Lock鎖。至關於整張表進行加鎖。這也對應着 「索引失效時行級鎖會退化成表級鎖」 的規律。

 

四、可串行化級別

讀操做會加讀鎖,寫操做會加寫鎖,讀寫鎖互斥。也會有間隙鎖。

1)用到主鍵索引和惟一索引,會對操做數據添加 Record Lock。

2)普通索引,會對操做數據以及間隙添加 Next-key Lock。

3)未使用索引,會對全部數據以及兩邊間隙添加 Next-key Lock。

 

 

MySQL 對幻讀的解決

「快照讀」 幻讀

經過上面對 MVCC 原理的解釋,能夠知道文章開頭的例子爲何「解決了」 幻讀。若是假設左邊的事務1 id 是50,右邊事務2 id 是55,其餘數據建立時的事務是10,那麼在事務1第一次查詢時生成的 Read View 的 trx_ids 爲[55],對應的數據以下

 那麼在判斷其餘數據時 trx_id 的10小於 trx_ids 的最小值55,因此經過,而 id 爲6的數據發現 trx_id 正好等於 55,因此獲取 roll_point 從 undo log中找到以前的數據快照,可是發現該列值爲空,因此放棄跳到下一條數據。沒有出現文章開頭所說的 「幻讀」 狀況,開頭所說的讀就叫作 「快照讀」 幻讀。 由此咱們能夠知道, MVCC 能夠解決 「快照度」 幻讀。

這裏能夠再說一下題外話,其實對於 MVCC 中可重複讀級別 Read View 建立時機爲何是第一次查詢時生成而不是事務啓動時就生成,能夠經過下面的測試來證實。

 能夠在事務2提交後再查詢就會查出提交後的數據。

 

「當前讀」 幻讀

這樣看來 MVCC 已經解決了幻讀問題,而在一開始也說過在默認時在必定程度上解決了幻讀,爲何這麼說?請看下面這個例子

 若是單看左邊的事務,會發現明明表中沒有id爲6的記錄,可是就是沒法執行 insert 操做,顯示主鍵已存在。這就是 「當前讀」幻讀,而 MVCC只能解決 「快照讀」 幻讀。因爲前面對 「當前讀」、「快照讀」 的解釋能夠知道這兩種讀是互斥的,那麼如何解決 「當前讀」 幻讀。第一種方式是直接切換成 「可串行化」 級別,這種由於默認對數據加鎖,不利於項目的併發執行,因此不建議;第二種就是手動添加鎖,在特定的操做後添加 for update 或 lock in share mode。這樣就能夠實現 "當前讀"了。

 

 

死鎖

MySQL 的死鎖與多線程中的死鎖本質上同樣,其核心思想就是 「兩個及以上的事務互相獲取對方事務添加的鎖記錄(排它鎖)」,

 

 

 

博客主要靈感來源於

https://blog.csdn.net/cug_jiang126com/article/details/50596729

http://www.javashuo.com/article/p-fjxhorgq-nw.html,其中一些圖片和加鎖狀況來源於第二個

相關文章
相關標籤/搜索