MySQL學習系列之InnoDB下事務隔離機制

林花鹿發表於2018-06-04

一. MySQL常用儲存引擎

MyISAM(MySQL 5.5.5 之前預設的儲存引擎)

特點:

  • 訪問速度快。
  • 不支援事務,因此適用不要求事務完整性或以select和insert為主。
  • 鎖粒度是支援併發的表級鎖,這樣的優點是加鎖快,開銷小,而對應的是併發性比較弱,容易引發鎖衝突。

應用場景

  1. 在讀寫操作非常頻繁的時候時候忌用,因為這樣容易產生大量的鎖衝突,形成大量的等待。
  2. 適用於查詢為主的應用場景。

InnoDB(MySQL 5.5.5及以後預設的儲存引擎)

特點:

  • 支援事務,支援回滾,因此適用於需要進行事務處理的應用場景
  • 鎖粒度是支援MVCC(這裡包含樂觀鎖的概念,下文進行解釋)的行級鎖,因此有很高的併發性
  • 支援外來鍵

應用場景:

  • 適用於需要事務操作,例如修改、刪除為主的專案中

ACID事務:

  • A事務的原子性(Atomicity):事務是一個不可分割的單元,事務中的所有操作,要麼全做完,要麼全不做

  • C 事務的一致性(Consistency):一致性,即在事務開始之前和事務結束以後,資料庫的完整性約束沒有被破壞。比如使用者A給使用者B轉賬,使用者B給使用者A轉賬,而約束就是兩個人的總金額還是一樣的

  • I 事務的隔離性(Isolation):多個事務併發訪問時,事務之間是隔離的,一個事務不應該影響其它事務執行效果。在下面會擴充套件MySQL事務隔離級別。

  • D 事務的永續性(Durability):意味著在事務完成以後,該事務所對資料庫所作的更改便持久的儲存在資料庫之中,並不會被回滾。

Archive

特點:

  • 不支援事務
  • 鎖粒度是行級鎖
  • 只支援insert和select

應用場景:

  • 適用於儲存操作日誌記錄的資料

二、InnoDB中事務隔離級別

什麼是髒讀,不可重複讀,幻讀

  • 髒讀: 對於兩個事務A,B,事務A讀取了已經被B更新但還沒有提交的欄位,之後,若B回滾,T1讀取到的內容就是臨時無效的內容。

  • 不可重複讀: 在事務A中,多次對同一資料進行讀取,此時事務B也對該資料進行訪問,也許事務B的修改,事務A多次獲得的資料可能不一樣。

  • 幻讀:事務A讀取與搜尋條件相匹配的若干行。事務B以插入或刪除行等方式來修改事務A的結果集,然後再提交。這裡幻讀與不可重複讀的差別在於不可重複讀強調的是修改和刪除,而幻讀強調的是插入

MySQL中如何避免髒讀、不可重複讀、幻讀

MySQL中存在四種隔離等級:

隔離級別 髒讀 不可重複讀 幻讀
未提交讀(Read uncommitted) 可能 可能 可能
已提交讀(Read committed) 不可能 可能 可能
可重複讀(Repeatable read) 不可能 不可能 可能
可序列讀(Serializable) 不可能 不可能 不可能
  • 未提交讀:允許髒讀,可能會讀取到別的事務中未提交的臨時資料
  • 已提交讀:只已經提交的資料,Oracle等資料庫預設是這個級別
  • 可重複讀:可重複讀,InnoDB預設的等級是這個,可是還是出現幻讀
  • 可序列讀:完全序列化的讀,表示每次加鎖都是表級鎖,而且讀寫相互阻塞,讀是共享鎖,寫是排他鎖

這裡思考一下,為啥阻止幻讀的隔離級別比不可重複讀的高,因為InnoDB是行級鎖,不可重複讀是針對於對於正在修改或刪除的資料行加鎖,但還是可以對錶進行插入,所以可能出現幻讀,要避免幻讀就要把表的讀寫都變成表級鎖,才能避免幻讀,也因此變成了隔離等級為“可序列讀”。

PS:以上內容以 悲觀鎖 的概念可以更好理解,不過實際中出於效能考慮,是用以樂觀鎖 為理論基礎的MVCC(多版本併發控制,Multi-Version Concurrency Control )

什麼是悲觀鎖,什麼是樂觀鎖

悲觀鎖:

悲觀鎖,正如其名,具有強烈的獨佔和排他特性。它指的是對資料被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個資料處理過程中,將資料處於鎖定狀態。悲觀鎖的實現,往往依靠資料庫提供的鎖機制(也只有資料庫層提供的鎖機制才能真正保證資料訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改資料)。
即在事務A中資料在進行讀取(共享鎖)的時候,其他事務不能進行修改(排他鎖),當在事務A的資料進行修改(排他鎖)的時候,不能進行讀取(共享鎖)。

優點:可以獨自佔有,直到執行操作完成,然後釋放鎖,為資料處理的安全提供了保障。

缺點:但是在效率方面,處理加鎖的機制會讓資料庫產生額外的開銷,還有增加產生死鎖的機會


樂觀鎖:

悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是資料庫 效能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。
而樂觀鎖就很好的解決了這個問題,樂觀鎖可以有兩種方式實現,一種是version,一種是時間戳,這裡以version為例,假設資料一般情況下不會發生衝突,只有在提交的時候,才會進行加鎖,並判斷這提交的事務的版本與當前資料庫的版本的對比,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料,則把請求駁回。

優點:

樂觀鎖機制避免了長事務中的資料庫加鎖開銷,大大提升了大併發量下的系統整體效能表現。

缺點:

如果是在高併發下,很多使用者容易出現衝突,即請求容易駁回


InnoDB中MVCC的實現:

MVCC的實現沒有固定的規範,每個資料庫中都會有不同的實現,這裡討論InnoDB的MVCC,其內部原理是通過樂觀鎖,在InnoDB中,會在每行資料後面新增兩個額外的隱藏的值來實現MVCC,一個是這個資料何時被建立,一個是這資料何時過期。在實際中,這裡儲存的是版本號,每開啟一個新的事務,事務的版本號就回增加。在可重讀Repeatable reads事務隔離級別下:

  1. SELECT時,讀取建立的版本號<=當前資料庫的版本。刪除版本為空或>當前當前事務版本的
  2. INSERT時,儲存當前事務版本為行的建立版本
  3. DELETE時,儲存當前版本為行的刪除版本
  4. UPDATE時,插入一條新資料。儲存當前事務版本為行建立版本,同一時候儲存當前事務版本到原來刪除的行
  5. 通過MVCC,儘管每行記錄都須要額外的儲存空間,很多其它的行檢查工作以及一些額外的維護工作。但能夠降低鎖的使用,大多數讀操作都不用加鎖,讀資料操作非常easy,效能非常好,而且也能保證僅僅會讀取到符合標準的行。也僅僅鎖住必要行。

通過MVCC,雖然每行記錄都需要額外的儲存空間,更多的行檢查工作以及一些額外的維護工作,但可以減少鎖的使用,大多數讀操作都不用加鎖,讀資料操作很簡單,效能很好,並且也能保證只會讀取到符合標準的行,也只鎖住必要行。

我們不管從資料庫方面的教課書中學到,還是從網路上看到,大都是上文中事務的四種隔離級別這一模組列出的意思,RR級別是可重複讀的,但無法解決幻讀,而只有在Serializable級別才能解決幻讀。於是我就加了一個事務C來展示效果。在事務C中新增了一條teacher_id=1的資料commit,RR級別中應該會有幻讀現象,事務A在查詢teacher_id=1的資料時會讀到事務C新加的資料。但是測試後發現,在MySQL中是不存在這種情況的,在事務C提交後,事務A還是不會讀到這條資料。可見在MySQL的RR級別中,是解決了幻讀的讀問題的。參見下圖

MVCC中RR隔離等級下解決幻讀示例圖


MVCC中可能讀取的之前版本的資料,要如何讀取當前資料呢?

這裡又引申出兩個概念:快照讀和當前讀
快照讀:就是普通的select

  • select * from table ...;

當前讀:特殊的讀操作(是要獲取鎖的),例如插入/更新/刪除就是當前讀

  • select * from table where ? lock in share mode;(共享鎖)
  • select * from table where ? for update;(排他鎖)
  • insert;
  • update;
  • delete;

事務的隔離級別實際上都是定義了當前讀的級別,MySQL為了減少鎖處理(包括等待其它鎖)的時間,提升併發能力,引入了快照讀的概念,使得select不用加鎖。事務的隔離只定義了讀資料的要求,而寫的要求當然是"當前讀"。

這裡注意,InnoDB預設的是行級鎖,行級鎖都是基於索引的。在當前讀的查詢語句中,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖把整張表鎖住,這點需要注意。

在MySQL中,insert、update、delete語句預設會對涉及的資料集加排他鎖,在Read committed等級下select語句預設是不會加的,如果要加的話,則需要顯示在後面加 lock in share mode。在Repeatable read以及在Serializable隔離機制下,select是加共享鎖的。


我是MySQL菜鳥,如果對於本文中有什麼疑問或者問題,歡迎互相討論提高

參考內容:
《深入理解樂觀鎖與悲觀鎖》
《Innodb中的事務隔離級別和鎖的關係》
《慕課網-資料庫設計那些事》
《MySQL常用資料儲存引擎區別》

相關文章