深入理解mysql四種隔離級別及底層實現原理(MVCC和鎖)

一、ACID特性

持久性,我們就不講了,易懂。

1、原子性

在同一個事務內部的一組操作必須全部執行成功(或者全部失敗)。

為了保證事務操作的原子性,必須實現基於日誌的REDO/UNDO機制:將所有對數據的更新操作都寫入日誌,如果一個事務中的一部分操作已經成功,但以後的操作,由於斷電/系統崩潰/其它的軟硬件錯誤而無法繼續,則通過回溯日誌,將已經執行成功的操作撤銷,從而達到“全部操作失敗”的目的。

最常見的場景是,數據庫系統崩潰後重啟,此時數據庫處於不一致的狀態,必須先執行一個crash recovery的過程:讀取日誌進行REDO(重演將所有已經執行成功但尚未寫入到磁盤的操作,保證持久性),再對所有到崩潰時尚未成功提交的事務進行UNDO(撤銷所有執行了一部分但尚未提交的操作,保證原子性)。crash recovery結束後,數據庫恢復到一致性狀態,可以繼續被使用。

某個應用在執行轉帳的數據庫操作時,必須在同一個事務內部調用對帳戶A和帳戶B的操作,才能保證數據的一致性。

但是,原子性並不能完全保證一致性。

在多個事務並行進行的情況下,即使保證了每一個事務的原子性,仍然可能導致數據不一致的結果。

例如,事務1需要將100元轉入帳號A:先讀取帳號A的值,然後在這個值上加上100。但是,在這兩個操作之間,另一個事務2修改了帳號A的值,為它增加了100元。那麼最後的結果應該是A增加了200元。但事實上,事務1最終完成後,帳號A只增加了100元,因為事務2的修改結果被事務1覆蓋掉了。

簡而言之,就是:原子性僅能夠保證單個事務的一致性。就像redis一樣,也只能保證單操作的線程安全,並不能保證多操作下的線程安全。

2、一致性

按照我個人的理解,在事務處理的ACID屬性中,一致性是最基本的屬性,其它的三個屬性都為了保證一致性而存在的。

我們舉個反例來理解下一致性概念。例如:從帳戶A轉一筆錢到帳戶B上,如果帳戶A上的錢減少了,而帳戶B上的錢卻沒有增加,那麼我們認為此時數據處於不一致的狀態。

為了保證併發情況下的一致性,引入了

隔離性,即保證每一個事務能夠看到的數據總是一致的,就好象其它併發事務並不存在一樣。

3、隔離性

數據庫四種隔離級別,以及常見的幾種讀異常,大家應該都是耳熟能詳的,但數據庫底層是怎麼實現隔離性的呢?都採用了哪些技術呢?

主要有兩個技術:MVCC(多版本併發控制)和鎖。

(1)MVCC(多版本併發控制)

多版本併發控制,顧名思義,在併發訪問的時候,數據存在版本的概念,可以有效地提升數據庫併發能力,常見的數據庫如MySQL、MS SQL Server、IBM DB2、Hbase、MongoDB等等都在使用。

簡單講,如果沒有MVCC,當想要讀取的數據被其他事務用排它鎖鎖住時,只能互斥等待;而這時MVCC可以通過提供歷史版本從而實現讀取被鎖的數據的歷史版本,從而避免了互斥等待。

InnoDB採用的MVCC實現方式是:在需要時,通過undo日誌構造出歷史版本。

(2)鎖

1) 鎖的分類

  • Shared Locks(共享鎖/S鎖)

若事務T對數據對象A加上S鎖,則事務T只能讀A;其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這就保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。

  • Exclusive Locks(排它鎖/X鎖)

若事務T對數據對象A加上X鎖,則只允許T讀取和修改A,其它任何事務都不能再對A加任何類型的鎖,直到T釋放A上的鎖。它防止任何其它事務獲取資源上的鎖,直到在事務的末尾將資源上的原始鎖釋放為止。在更新操作(INSERT、UPDATE 或 DELETE)過程中始終應用排它鎖。

注意:排他鎖會阻止其它事務再對其鎖定的數據加讀或寫的鎖,但是不加鎖的就沒辦法控制了。

  • Record Locks(行鎖)

行鎖,顧名思義,是加在索引行(對!是索引行!不是數據行!)上的鎖。比如select * from user where id=1 and id=10 for update,就會在id=1和id=10的索引行上加Record Lock。

  • Gap Locks(間隙鎖)

間隙鎖,它會鎖住兩個索引之間的區域。比如select * from user where id>1 and id<10 for update,就會在id為(1,10)的索引區間上加Gap Lock。

  • Next-Key Locks(間隙鎖)

也叫間隙鎖,它是Record Lock + Gap Lock形成的一個閉區間鎖。比如select * from user where id>=1 and id<=10 for update,就會在id為[1,10]的索引閉區間上加Next-Key Lock。

這樣組合起來就有,行級共享鎖,表級共享鎖,行級排它鎖,表級排它鎖。

2) 什麼時候會加鎖?

在數據庫增刪改查四種操作中,insert、delete和update都是會加排它鎖(Exclusive Locks)的,而select只有顯式聲明才會加鎖:

  • select: 即最常用的查詢,是不加任何鎖的
  • select ... lock in share mode: 會加共享鎖(Shared Locks)
  • select ... for update: 會加排它鎖

3) 四種隔離級別

不同的隔離級別是在數據可靠性和併發性之間的均衡取捨,隔離級別越高,對應的併發性能越差,數據越安全可靠。

  • READ UNCOMMITTED

顧名思義,事務之間可以讀取彼此未提交的數據。機智如你會記得,在前文有說到所有寫操作都會加排它鎖,那還怎麼讀未提交呢?

機智如你,前面我們介紹排它鎖的時候,有這種說明:

排他鎖會阻止其它事務再對其鎖定的數據加讀或寫的鎖,但是對不加鎖的讀就不起作用了。

READ UNCOMMITTED隔離級別下, 讀不會加任何鎖。而寫會加排他鎖,併到事務結束之後釋放。

實例1:

查-寫:查並沒有阻止寫,表明查肯定並沒有加鎖,要不寫肯定就阻塞了。寫很明顯,會加排它鎖的。

實例2:

寫-寫:阻塞,表明,寫會加排它鎖。

  • READ COMMITTED

顧名思義,事務之間可以讀取彼此已提交的數據。

InnoDB在該隔離級別(READ COMMITTED)寫數據時,使用排它鎖, 讀取數據不加鎖而是使用了MVCC機制。

因此,在讀已提交的級別下,都會通過MVCC獲取當前數據的最新快照,不加任何鎖,也無視任何鎖(因為歷史數據是構造出來的,身上不可能有鎖)。

但是,該級別下還是遺留了不可重複讀和幻讀問題:

MVCC版本的生成時機: 是每次select時。這就意味著,如果我們在事務A中執行多次的select,在每次select之間有其他事務更新了我們讀取的數據並提交了,那就出現了不可重複讀,即:重複讀時,會出現數據不一致問題,後面我們會講解超支現象,就是這種引起的。

  • REPEATABLE READ

READ COMMITTED級別不同的是MVCC版本的生成時機,即:一次事務中只在第一次select時生成版本,後續的查詢都是在這個版本上進行,從而實現了可重複讀

但是因為MVCC的快照只對讀操作有效,對寫操作無效,舉例說明會更清晰一點:

事務A依次執行如下3條sql,事務B在語句1和2之間,插入10條age=20的記錄,事務A就幻讀了。

1. select count(1) from user where age=20;
-- return 0: 當前沒有age=20的
2. update user set name=test where age=20;
-- Affects 10 rows: 因為事務B剛寫入10條age=20的記錄,而寫操作是不受MVCC影響,能看到最新數據的,所以更新成功,而一旦操作成功,這些被操作的數據就會對當前事務可見
3. select count(1) from user where age=20;
-- return 10: 出現幻讀

REPEATABLE READ級別,可以防止大部分的幻讀,但像前邊舉例讀-寫-讀的情況,使用不加鎖的select依然會幻讀。

  • SERIALISABLE

大殺器,該級別下,會自動將所有普通select轉化為select ... lock in share mode執行,即針對同一數據的所有讀寫都變成互斥的了,可靠性大大提高,併發性大大降低。

讀-寫,寫-寫均互斥。

4)總結:幾類讀異常

讀-寫-讀,引起的異常

  • 髒讀:讀取了髒數據(不存在的數據)。
  • 事務一讀
  • 事務二寫(未提交)
  • 事務二讀(髒數據)
  • 事務二回滾
  • 不可重複讀:既可以讀取修改的數據,也可以讀取新增的數據(幻讀)。
  • 事務一讀
  • 事務二寫(更新已提交)
  • 事務二讀(數據不一致,不可重複讀)
  • 幻讀:僅可以讀取新增的數據,但是無法讀取修改的數據;
  • 事務一讀
  • 事務二寫(新增已提交)
  • 事務二讀(數據不一致,幻讀)
  • 附命令

查看錶的加鎖情況: select * from information_schema.INNODB_LOCKS;

事務狀態 select * from information_schema.INNODB_TRX;


分享到:


相關文章: