深刻理解MySQL中事務隔離級別的實現原理

前言

說到數據庫事務,你們腦子裏必定很容易蹦出一堆事務的相關知識,如事務的ACID特性,隔離級別,解決的問題(髒讀,不可重複讀,幻讀)等等,可是可能不多有人真正的清楚事務的這些特性又是怎麼實現的,爲何要有四個隔離級別。html

今天咱們就先來聊聊MySQL中事務的隔離性的實現原理,後續還會繼續出文章分析其餘特性的實現原理。mysql

固然MySQL博大精深,文章疏漏之處在所不免,歡迎批評指正。sql

說明數據庫

MySQL的事務實現邏輯是位於引擎層的,而且不是全部的引擎都支持事務的,下面的說明都是以InnoDB引擎爲基準。segmentfault

定義

隔離性(isolation)指的是不一樣事務前後提交併執行後,最終呈現出來的效果是串行的,也就是說,對於事務來講,它在執行過程當中,感知到的數據變化應該只有本身操做引發的,不存在其餘事務引起的數據變化。併發

隔離性解決的是併發事務出現的問題code

標準SQL隔離級別

隔離性最簡單的實現方式就是各個事務都串行執行了,若是前面的事務尚未執行完畢,後面的事務就都等待。可是這樣的實現方式很明顯併發效率不高,並不適合在實際環境中使用。htm

爲了解決上述問題,實現不一樣程度的併發控制,SQL的標準制定者提出了不一樣的隔離級別:未提交讀(read uncommitted)、提交讀(read committed)、可重複讀(repeatable read)、序列化讀(serializable)。其中最高級隔離級別就是序列化讀,而在其餘隔離級別中,因爲事務是併發執行的,因此或多或少容許出現一些問題。見如下的矩陣表:事務

隔離級別(+:容許出現,-:不容許出現) 髒讀 不可重複讀 幻讀
未提交讀                                  +         +               +        
提交讀                                    -         +               +        
可重複讀                                  -         -               +        
序列化讀                                  -         -               -        

注意,MySQL的InnoDB引擎在提交讀級別經過MVCC解決了不可重複讀的問題,在可重複讀級別經過間隙鎖解決了幻讀問題,具體見下面的分析文檔

實現原理

標準SQL事務隔離級別實現原理

咱們上面遇到的問題其實就是併發事務下的控制問題,解決併發事務的最多見方式就是悲觀併發控制了(也就是數據庫中的鎖)。標準SQL事務隔離級別的實現是依賴鎖的,咱們來看下具體是怎麼實現的:

事務隔離級別    實現方式                                                     
未提交讀(RU) 事務對當前被讀取的數據不加鎖;

事務在更新某數據的瞬間(就是發生更新的瞬間),必須先對其加行級共享鎖,直到事務結束才釋放。
提交讀(RC)    事務對當前被讀取的數據加行級共享鎖(當讀到時才加鎖),一旦讀完該行,當即釋放該行級共享鎖;

事務在更新某數據的瞬間(就是發生更新的瞬間),必須先對其加行級排他鎖,直到事務結束才釋放。
可重複讀(RR) 事務在讀取某數據的瞬間(就是開始讀取的瞬間),必須先對其加行級共享鎖,直到事務結束才釋放;

事務在更新某數據的瞬間(就是發生更新的瞬間),必須先對其加行級排他鎖,直到事務結束才釋放。
序列化讀(S)   事務在讀取數據時,必須先對其加表級共享鎖 ,直到事務結束才釋放;

事務在更新數據時,必須先對其加表級排他鎖 ,直到事務結束才釋放。

能夠看到,在只使用鎖來實現隔離級別的控制的時候,須要頻繁的加鎖解鎖,並且很容易發生讀寫的衝突(例如在RC級別下,事務A更新了數據行1,事務B則在事務A提交前讀取數據行1都要等待事務A提交併釋放鎖)。

爲了避免加鎖解決讀寫衝突的問題,MySQL引入了MVCC機制,詳細可見我之前的分析文章:一文讀懂數據庫中的樂觀鎖和悲觀鎖和MVCC

InnoDB事務隔離級別實現原理

在往下分析以前,咱們有幾個概念須要先了解下:

一、鎖定讀和一致性非鎖定讀

鎖定讀:在一個事務中,主動給讀加鎖,如SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。分別加上了行共享鎖和行排他鎖。鎖的分類可見我之前的分析文章:你應該瞭解的MySQL鎖分類)。

https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html

一致性非鎖定讀:InnoDB使用MVCC向事務的查詢提供某個時間點的數據庫快照。查詢會看到在該時間點以前提交的事務所作的更改,而不會看到稍後或未提交的事務所作的更改(本事務除外)。也就是說在開始了事務以後,事務看到的數據就都是事務開啓那一刻的數據了,其餘事務的後續修改不會在本次事務中可見。

Consistent read是InnoDB在RC和RR隔離級別處理SELECT語句的默認模式。一致性非鎖定讀不會對其訪問的表設置任何鎖,所以,在對錶執行一致性非鎖定讀的同時,其它事務能夠同時併發的讀取或者修改它們。

https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html

二、當前讀和快照讀

當前讀

讀取的是最新版本,像UPDATE、DELETE、INSERT、SELECT ...  LOCK IN SHARE MODE、SELECT ... FOR UPDATE這些操做都是一種當前讀,爲何叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其餘併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。

快照讀

讀取的是快照版本,也就是歷史版本,像不加鎖的SELECT操做就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是未提交讀和序列化讀級別,由於未提交讀老是讀取最新的數據行,而不是符合當前事務版本的數據行,而序列化讀則會對錶加鎖

三、隱式鎖定和顯式鎖定

隱式鎖定

InnoDB在事務執行過程當中,使用兩階段鎖協議(不主動進行顯示鎖定的狀況):

  • 隨時均可以執行鎖定,InnoDB會根據隔離級別在須要的時候自動加鎖;
  • 鎖只有在執行commit或者rollback的時候纔會釋放,而且全部的鎖都是在同一時刻被釋放。

顯式鎖定

  • InnoDB也支持經過特定的語句進行顯示鎖定(存儲引擎層)
select ... lock in share mode //共享鎖
select ... for update //排他鎖
  • MySQL Server層的顯示鎖定:
lock table
unlock table

瞭解完上面的概念後,咱們來看下InnoDB的事務具體是怎麼實現的(下面的讀都指的是非主動加鎖的select)

事務隔離級別    實現方式                                                     
未提交讀(RU) 事務對當前被讀取的數據不加鎖,都是當前讀

事務在更新某數據的瞬間(就是發生更新的瞬間),必須先對其加行級共享鎖,直到事務結束才釋放。
提交讀(RC)    事務對當前被讀取的數據不加鎖,且是快照讀

事務在更新某數據的瞬間(就是發生更新的瞬間),必須先對其加行級排他鎖(Record),直到事務結束才釋放。

經過快照,在這個級別MySQL就解決了不可重複讀的問題
可重複讀(RR) 事務對當前被讀取的數據不加鎖,且是快照讀

事務在更新某數據的瞬間(就是發生更新的瞬間),必須先對其加行級排他鎖(Record,GAP,Next-Key),直到事務結束才釋放。

經過間隙鎖,在這個級別MySQL就解決了幻讀的問題
序列化讀(S)   事務在讀取數據時,必須先對其加表級共享鎖 ,直到事務結束才釋放,都是當前讀

事務在更新數據時,必須先對其加表級排他鎖 ,直到事務結束才釋放。

能夠看到,InnoDB經過MVCC很好的解決了讀寫衝突的問題,並且提早一個級別就解決了標準級別下會出現的幻讀和不可重複讀問題,大大提高了數據庫的併發能力。

一些常見誤區

幻讀到底包不包括了delete的狀況?

不可重複讀:先後屢次讀取一行,數據內容不一致,針對其餘事務的update和delete操做。爲了解決這個問題,使用行共享鎖,鎖定到事務結束(也就是RR級別,固然MySQL使用MVCC在RC級別就解決了這個問題)

幻讀:當同一個查詢在不一樣時間生成不一樣的行集合時就是出現了幻讀,針對的是其餘事務的insert操做,爲了解決這個問題,鎖定整個表到事務結束(也就是S級別,固然MySQL使用間隙鎖在RR級別就解決了這個問題)

網上不少文章提到幻讀和提交讀的時候,有的說幻讀包括了delete的狀況,有的說delete應該屬於提交讀的問題,那到底真相如何呢?咱們實際來看下MySQL的官方文檔(以下)

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT) is executed twice, but returns a row the second time that was not returned the first time, the row is a 「phantom」 row.

https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html

能夠看到,幻讀針對的是結果集先後發生變化,因此看起來delete的狀況應該歸爲幻讀,可是咱們實際分析下上面列出的標準SQL在RR級別的實現原理就知道,標準SQL的RR級別是會對查到的數據行加行共享鎖,因此這時候其餘事務想刪除這些數據行實際上是作不到的,因此在RR下,不會出現因delete而出現幻讀現象,也就是幻讀不包含delete的狀況。

MVCC能解決了幻讀問題?

網上不少文章會說MVCC或者MVCC+間隙鎖解決了幻讀問題,實際上MVCC並不能解決幻讀問題。如如下的例子:

begin;

#假設users表爲空,下面查出來的數據爲空

select * from users; #沒有加鎖

#此時另外一個事務提交了,且插入了一條id=1的數據

select * from users; #讀快照,查出來的數據爲空

update users set name='mysql' where id=1;#update是當前讀,因此更新成功,並生成一個更新的快照

select * from users; #讀快照,查出來id爲1的一條記錄,由於MVCC能夠查到當前事務生成的快照

commit;

能夠看到先後查出來的數據行不一致,發生了幻讀。因此說只有MVCC是不能解決幻讀問題的,解決幻讀問題靠的是間隙鎖。以下:

begin;

#假設users表爲空,下面查出來的數據爲空

select * from users lock in share mode; #加上共享鎖

#此時另外一個事務B想提交且插入了一條id=1的數據,因爲有間隙鎖,因此要等待

select * from users; #讀快照,查出來的數據爲空

update users set name='mysql' where id=1;#update是當前讀,因爲不存在數據,不進行更新

select * from users; #讀快照,查出來的數據爲空

commit;

#事務B提交成功並插入數據

注意,RR級別下想解決幻讀問題,須要咱們顯式加鎖,否則查詢的時候仍是不會加鎖的

版權聲明

轉載請註明做者和文章出處
做者: X先生
http://www.javashuo.com/article/p-clughppx-ns.html

以爲不錯的話請幫忙收藏點贊~

相關文章
相關標籤/搜索