隔離級別(+:允許出現,-:不允許出現) | 臟讀 | 不可重復讀 | 幻讀 |
---|---|---|---|
未提交讀 | + | + | + |
提交讀 | - | + | + |
可重復讀 | - | - | + |
序列化讀 | - | - | - |
注意,MySQL的InnoDB引擎在提交讀級別通過MVCC解決了不可重復讀的問題,在可重復讀級別通過間隙鎖解決了幻讀問題,具體見下面的分析。
我們上面遇到的問題其實就是并發(fā)事務下的控制問題,解決并發(fā)事務的最常見方式就是悲觀并發(fā)控制了(也就是數據庫中的鎖)。標準SQL事務隔離級別的實現是依賴鎖的,我們來看下具體是怎么實現的:
事務隔離級別 | 實現方式 |
---|---|
未提交讀(RU) | 事務對當前被讀取的數據不加鎖; 事務在更新某數據的瞬間(就是發(fā)生更新的瞬間),必須先對其加行級共享鎖,直到事務結束才釋放。 |
提交讀(RC) | 事務對當前被讀取的數據加行級共享鎖(當讀到時才加鎖),一旦讀完該行,立即釋放該行級共享鎖; 事務在更新某數據的瞬間(就是發(fā)生更新的瞬間),必須先對其加行級排他鎖,直到事務結束才釋放。 |
可重復讀(RR) | 事務在讀取某數據的瞬間(就是開始讀取的瞬間),必須先對其加行級共享鎖,直到事務結束才釋放; 事務在更新某數據的瞬間(就是發(fā)生更新的瞬間),必須先對其加行級排他鎖,直到事務結束才釋放。 |
序列化讀(S) | 事務在讀取數據時,必須先對其加表級共享鎖 ,直到事務結束才釋放; 事務在更新數據時,必須先對其加表級排他鎖 ,直到事務結束才釋放。 |
可以看到,在只使用鎖來實現隔離級別的控制的時候,需要頻繁的加鎖解鎖,而且很容易發(fā)生讀寫的沖突(例如在RC級別下,事務A更新了數據行1,事務B則在事務A提交前讀取數據行1都要等待事務A提交并釋放鎖)。
為了不加鎖解決讀寫沖突的問題,MySQL引入了MVCC機制,詳細可見我以前的分析文章:一文讀懂數據庫中的樂觀鎖和悲觀鎖和MVCC。
在往下分析之前,我們有幾個概念需要先了解下:
1、鎖定讀和一致性非鎖定讀
鎖定讀:在一個事務中,主動給讀加鎖,如SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。分別加上了行共享鎖和行排他鎖。鎖的分類可見我以前的分析文章:你應該了解的MySQL鎖分類)。
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html
一致性非鎖定讀:InnoDB使用MVCC向事務的查詢提供某個時間點的數據庫快照。查詢會看到在該時間點之前提交的事務所做的更改,而不會看到稍后或未提交的事務所做的更改(本事務除外)。也就是說在開始了事務之后,事務看到的數據就都是事務開啟那一刻的數據了,其他事務的后續(xù)修改不會在本次事務中可見。
Consistent read是InnoDB在RC和RR隔離級別處理SELECT語句的默認模式。一致性非鎖定讀不會對其訪問的表設置任何鎖,因此,在對表執(zhí)行一致性非鎖定讀的同時,其它事務可以同時并發(fā)的讀取或者修改它們。
https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html
2、當前讀和快照讀
當前讀
讀取的是最新版本,像UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE這些操作都是一種當前讀,為什么叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他并發(fā)事務不能修改當前記錄,會對讀取的記錄進行加鎖。
快照讀
讀取的是快照版本,也就是歷史版本,像不加鎖的SELECT操作就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是未提交讀和序列化讀級別,因為未提交讀總是讀取最新的數據行,而不是符合當前事務版本的數據行,而序列化讀則會對表加鎖。
3、隱式鎖定和顯式鎖定
隱式鎖定
InnoDB在事務執(zhí)行過程中,使用兩階段鎖協議(不主動進行顯示鎖定的情況):
顯式鎖定
select ... lock in share mode //共享鎖 select ... for update //排他鎖
lock table unlock table
了解完上面的概念后,我們來看下InnoDB的事務具體是怎么實現的(下面的讀都指的是非主動加鎖的select)
事務隔離級別 | 實現方式 |
---|---|
未提交讀(RU) | 事務對當前被讀取的數據不加鎖,都是當前讀; 事務在更新某數據的瞬間(就是發(fā)生更新的瞬間),必須先對其加行級共享鎖,直到事務結束才釋放。 |
提交讀(RC) | 事務對當前被讀取的數據不加鎖,且是快照讀; 事務在更新某數據的瞬間(就是發(fā)生更新的瞬間),必須先對其加行級排他鎖(Record),直到事務結束才釋放。 通過快照,在這個級別MySQL就解決了不可重復讀的問題 |
可重復讀(RR) | 事務對當前被讀取的數據不加鎖,且是快照讀; 事務在更新某數據的瞬間(就是發(fā)生更新的瞬間),必須先對其加行級排他鎖(Record,GAP,Next-Key),直到事務結束才釋放。 通過間隙鎖,在這個級別MySQL就解決了幻讀的問題 |
序列化讀(S) | 事務在讀取數據時,必須先對其加表級共享鎖 ,直到事務結束才釋放,都是當前讀; 事務在更新數據時,必須先對其加表級排他鎖 ,直到事務結束才釋放。 |
可以看到,InnoDB通過MVCC很好的解決了讀寫沖突的問題,而且提前一個級別就解決了標準級別下會出現的幻讀和不可重復讀問題,大大提升了數據庫的并發(fā)能力。
不可重復讀:前后多次讀取一行,數據內容不一致,針對其他事務的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
可以看到,幻讀針對的是結果集前后發(fā)生變化,所以看起來delete的情況應該歸為幻讀,但是我們實際分析下上面列出的標準SQL在RR級別的實現原理就知道,標準SQL的RR級別是會對查到的數據行加行共享鎖,所以這時候其他事務想刪除這些數據行其實是做不到的,所以在RR下,不會出現因delete而出現幻讀現象,也就是幻讀不包含delete的情況。
網上很多文章會說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;
可以看到前后查出來的數據行不一致,發(fā)生了幻讀。所以說只有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級別下想解決幻讀問題,需要我們顯式加鎖,不然查詢的時候還是不會加鎖的
以上就是詳解MySQL中事務隔離級別的實現原理的詳細內容,更多關于MySQL 事務隔離級別的資料請關注腳本之家其它相關文章!