天天看點

事務隔離級别中的可重複讀能防幻讀嗎?

每次談到資料庫的事務隔離級别,大家一定會看到這張表.

事務隔離級别中的可重複讀能防幻讀嗎?

其中,<code>可重複讀</code>這個隔離級别,有效地防止了髒讀和不可重複讀,但仍然可能發生幻讀,可能發生幻讀就表示<code>可重複讀</code>這個隔離級别防不住幻讀嗎?

我不管從資料庫方面的教科書還是一些網絡教程上,經常看到RR級别是可以重複讀的,但是無法解決幻讀,隻有可串行化(Serializable)才能解決幻讀,這個說法是否正确呢?

在這篇文章中,我将重點圍繞MySQL中

可重複讀(Repeatable read)能防住幻讀嗎?

這一問題展開讨論,相信看完這篇文章後,你一定會對事務隔離級别有新的認識.

我們的資料庫中有如下結構和資料的<code>Users</code>表,下文中我們将對這張表進行操作,

事務隔離級别中的可重複讀能防幻讀嗎?

長文預警,讀完此篇文章,大概需要您二十至三十分鐘.

在說幻讀之前,我們要先來了解髒讀和不可重複讀.

當一個事務讀取到另外一個事務修改但未送出的資料時,就可能發生髒讀。

事務隔離級别中的可重複讀能防幻讀嗎?

在我們的例子中,事務2修改了一行,但是沒有送出,事務1讀了這個沒有送出的資料。現在如果事務2復原了剛才的修改或者做了另外的修改的話,事務1中查到的資料就是不正确的了,是以這條資料就是髒讀。

“不可重複讀”現象發生在當執行SELECT 操作時沒有獲得讀鎖或者SELECT操作執行完後馬上釋放了讀鎖; 另外一個事務對資料進行了更新,讀到了不同的結果.

事務隔離級别中的可重複讀能防幻讀嗎?

在這個例子中,事務2送出成功,是以他對id為1的行的修改就對其他事務可見了。導緻了事務1在此前讀的age=1,第二次讀的age=2,兩次結果不一緻,這就是不可重複讀.

“幻讀”又叫"幻象讀",是''不可重複讀''的一種特殊場景:當事務1兩次執行''SELECT ... WHERE''檢索一定範圍内資料的操作中間,事務2在這個表中建立了(如[[INSERT]])了一行新資料,這條新資料正好滿足事務1的“WHERE”子句。

事務隔離級别中的可重複讀能防幻讀嗎?

如圖事務1執行了兩遍同樣的查詢語句,第二遍比第一遍多出了一條資料,這就是幻讀。

三者的場景介紹完,但是一定仍然有很多同學搞不清楚,它們到底有什麼差別,我總結一下.

髒讀:指讀到了其他事務未送出的資料.

不可重複讀: 讀到了其他事務已送出的資料(update).

不可重複讀與幻讀都是讀到其他事務已送出的資料,但是它們針對點不同.

不可重複讀:update.

幻讀:delete,insert.

未送出讀(READ UNCOMMITTED)是最低的隔離級别,在這種隔離級别下,如果一個事務已經開始寫資料,則另外一個事務則不允許同時進行寫操作,但允許其他事務讀此行資料.

把髒讀的圖拿來分析分析,因為事務2更新id=1的資料後,仍然允許事務1讀取該條資料,是以事務1第二次執行查詢,讀到了事務2更新的結果,産生了髒讀.

事務隔離級别中的可重複讀能防幻讀嗎?

由于MySQL的InnoDB預設是使用的RR級别,是以我們先要将該session開啟成RC級别,并且設定binlog的模式

在已送出讀(READ COMMITTED)級别中,讀取資料的事務允許其他事務繼續通路該行資料,但是未送出的寫事務将會禁止其他事務通路該行,會對該寫鎖一直保持直到到事務送出.

同樣,我們來分析髒讀,事務2更新id=1的資料後,在送出前,會對該對象寫鎖,是以事務1讀取id=1的資料時,會一直等待事務2結束,處于阻塞狀态,避免了産生髒讀.

事務隔離級别中的可重複讀能防幻讀嗎?

同樣,來分析不可重複讀,事務1讀取id=1的資料後并沒有鎖住該資料,是以事務2能對這條資料進行更新,事務2對更新并送出後,該資料立即生效,是以事務1再次執行同樣的查詢,查詢到的結果便與第一次查到的不同,是以已送出讀防不了不可重複讀.

事務隔離級别中的可重複讀能防幻讀嗎?

在可重複讀(REPEATABLE READS)是介于已送出讀和可串行化之間的一種隔離級别(廢話😅),它是InnoDb的預設隔離級别,它是我這篇文章的重點讨論對象,是以在這裡我先賣個關子,後面我會詳細介紹.

可串行化(Serializable )是高的隔離級别,它求在標明對象上的讀鎖和寫鎖保持直到事務結束後才能釋放,是以能防住上訴所有問題,但因為是串行化的,是以效率較低.

事務隔離級别中的可重複讀能防幻讀嗎?

了解到了上訴的一些背景知識後,下面正式開始我們的議題.

在講可重複讀之前,我們先在mysql的InnoDB下做下面的實驗.

事務隔離級别中的可重複讀能防幻讀嗎?

可以看到,事務A既沒有讀到事務B更新的資料,也沒有讀到事務C添加的資料,是以在這個場景下,它既防住了不可重複讀,也防住了幻讀.

到此為止,相信大家已經知道答案了,這是怎麼做到的呢?

我們前面說的在對象上加鎖,是一種悲觀鎖機制,有很多文章說<code>可重複讀</code>的隔離級别防不了幻讀, 是認為可重複讀會對讀的行加鎖,導緻他事務修改不了這條資料,直到事務結束,但是這種方案隻能鎖住資料行,如果有新的資料進來,是阻止不了的,是以會産生幻讀.

可是MySQL、ORACLE、PostgreSQL等已經是非常成熟的資料庫了,怎麼會單純地采用這種如此影響性能的方案呢?

事務隔離級别中的可重複讀能防幻讀嗎?

我來介紹一下悲觀鎖和樂觀鎖.

正如其名,它指的是對資料被外界(包括本系統目前的其他事務,以及來自外部系統的事務處理)修改持保守态度,是以,在整個資料處理過程中,将資料處于鎖定狀态。讀取資料時給加鎖,其它事務無法修改這些資料。修改删除資料時也要加鎖,其它事務無法讀取這些資料。

相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠資料庫的鎖機制實作,以保證操作最大程度的獨占性。但随之而來的就是資料庫性能的大量開銷,特别是對長事務而言,這樣的開銷往往無法承受。

而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于資料版本( Version )記錄機制實作。何謂資料版本?即為資料增加一個版本辨別,在基于資料庫表的版本解決方案中,一般是通過為資料庫表增加一個 “version” 字段來實作。讀取出資料時,将此版本号一同讀出,之後更新時,對此版本号加一。此時,将送出資料的版本資料與資料庫表對應記錄的目前版本資訊進行比對,如果送出的資料版本号大于資料庫表目前版本号,則予以更新,否則認為是過期資料。

MySQL、ORACLE、PostgreSQL等都是使用了以樂觀鎖為理論基礎的MVCC(多版本并發控制)來避免不可重複讀和幻讀,MVCC的實作沒有固定的規範,每個資料庫都會有不同的實作方式,這裡讨論的是InnoDB的MVCC。

在InnoDB中,會在每行資料後添加兩個額外的隐藏的值來實作MVCC,這兩個值一個記錄這行資料何時被建立,另外一個記錄這行資料何時過期(或者被删除)。 在實際操作中,存儲的并不是時間,而是事務的版本号,每開啟一個新事務,事務的版本号就會遞增。 在可重讀Repeatable reads事務隔離級别下:

SELECT時,讀取建立版本号&lt;=目前事務版本号,删除版本号為空或&gt;目前事務版本号。

INSERT時,儲存目前事務版本号為行的建立版本号

DELETE時,儲存目前事務版本号為行的删除版本号

UPDATE時,插入一條新紀錄,儲存目前事務版本号為行建立版本号,同時儲存目前事務版本号到原來删除的行

事務隔離級别中的可重複讀能防幻讀嗎?

通過MVCC,雖然每行記錄都要額外的存儲空間來記錄version,需要更多的行檢查工作以及一些額外的維護工作,但可以減少鎖的使用,大多讀操作都不用加鎖,讀取資料操作簡單,性能好.

細心的同學應該也看到了,通過MVCC讀取出來的資料其實是曆史資料,而不是最新資料,這在一些對于資料時效特别敏感的業務中,很可能出問題,這也是MVCC的短闆之處,有辦法解決嗎? 當然有.

MCVV這種讀取曆史資料的方式稱為快照讀(snapshot read),而讀取資料庫目前版本資料的方式,叫目前讀(current read).

我們平時隻用使用select就是快照讀,這樣可以減少加鎖所帶來的開銷.

對于會對資料修改的操作(update、insert、delete)都是采用目前讀的模式。在執行這幾個操作時會讀取最新的記錄,即使是别的事務送出的資料也可以查詢到。假設要update一條記錄,但是在另一個事務中已經delete掉這條資料并且commit了,如果update就會産生沖突,是以在update的時候需要知道最新的資料。讀取的是最新的資料,需要加鎖。以下第一個語句需要加共享鎖,其它都需要加排它鎖。

我們再利用目前讀來做試驗.

事務隔離級别中的可重複讀能防幻讀嗎?

可以看到在讀送出的隔離級别中,事務1修改了所有class_id=1的資料,當時當事務2 insert後,事務A莫名奇妙地多了一行class_id=1的資料,而且沒有被之前的update所修改,産生了讀送出下的的幻讀.

而在可重複度的隔離級别下,情況就完全不同了.事務1在update後,對該資料加鎖,事務B無法插入新的資料,這樣事務A在update前後資料保持一緻,避免了幻讀,可以明确的是,update鎖的肯定不隻是已查詢到的幾條資料,因為這樣無法阻止insert,有同學會說,那就是鎖住了整張表呗.

還是那句話, Mysql已經是個成熟的資料庫了,怎麼會采用如此低效的方法呢? 其實這裡的鎖,是通過next-key鎖實作的.

在Users這張表裡面,class_id是個非聚簇索引,資料庫會通過B+樹維護一個非聚簇索引與主鍵的關系,簡單來說,我們先通過class_id=1找到這個索引所對應所有節點,這些節點存儲着對應資料的主鍵資訊,即id=1,我們再通過主鍵id=1找到我們要的資料,這個過程稱為回表.

不懂資料庫索引的底層原理?那是因為你心裡沒點b樹

前往學習: https://www.cnblogs.com/sujing/p/11110292.html

我本想用我們文章中的例子來畫一個B+樹,可是畫得太醜了,為了避免拉低此偏文章B格.是以我想引用上面那邊文章中作者畫的B+樹來解釋Next-key.

假設我們上面用到的User表需要對<code>Name</code>建立非聚簇索引,是怎麼實作的呢?我們看下圖:

事務隔離級别中的可重複讀能防幻讀嗎?

B+樹的特點是所有資料都存儲在葉子節點上,以非聚簇索引的<code>秦壽生</code>為例,在<code>秦壽生</code>的右葉子節點存儲着所有<code>秦壽生</code>對應的Id,即圖中的34,在我們對這條資料做了目前讀後,就會對這條資料加行鎖,對于行鎖很好了解,能夠防止其他事務對其進行<code>update</code>或<code>delete</code>,但為什麼要加GAP鎖呢?

還是那句話,B+樹的所有資料存儲在葉子節點上,當有一個新的叫<code>秦壽生</code>的資料進來,一定是排在在這條id=34的資料前面或者後面的,我們如果對前後這個範圍進行加鎖了,那當然新的<code>秦壽生</code>就插不進來了.

那如果有一個新的<code>範統</code>要插進行呢? 因為<code>範統</code>的前後并沒有被鎖住,是能成功插入的,這樣就極大地提高了資料庫的并發能力.

上文中說了可重複讀能防不可重複讀,還能防幻讀,它能防住所有的幻讀嗎?當然不是,也有馬失前蹄的時候.

比如如下的例子:

事務隔離級别中的可重複讀能防幻讀嗎?

1.a事務先select,b事務insert确實會加一個gap鎖,但是如果b事務commit,這個gap鎖就會釋放(釋放後a事務可以随意操作),

2.a事務再select出來的結果在MVCC下還和第一次select一樣,

3.接着a事務不加條件地update,這個update會作用在所有行上(包括b事務新加的),

4.a事務再次select就會出現b事務中的新行,并且這個新行已經被update修改了.

Mysql官方給出的幻讀解釋是:隻要在一個事務中,第二次select多出了row就算幻讀, 是以這個場景下,算出現幻讀了.

那麼文章最後留個問題,你知道為什麼上訴例子會出現幻讀嗎?歡迎留言讨論.

參考文章:

MySQL 5.6 Reference Manual

understanding InnoDB transaction isolation levels

MySQL · 源碼分析 · InnoDB Repeatable Read隔離級别之大不同

Innodb中的事務隔離級别和鎖的關系

MySQL InnoDB中的行鎖 Next-Key Lock消除幻讀

繼續閱讀