12.24 面試官:MySQL的可重複讀級別能解決幻讀問題嗎?

juejin.im/post/5c9040e95188252d92095a9e

引言

之前在深入瞭解數據庫理論的時候,瞭解到事務的不同隔離級別可能存在的問題。為了更好的理解所以在MySQL數據庫中測試復現這些問題。關於髒讀和不可重複讀在相應的隔離級別下都很容易的復現了。

但是對於幻讀,我發現在可重複讀的隔離級別下沒有出現,當時想到難道是MySQL對幻讀做了什麼處理?

測試:

創建一張測試用的表dept:

<code>CREATETABLE`dept`(
`id`int(11)NOTNULLAUTO_INCREMENT,
`name`varchar(20)DEFAULTNULL,
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=12DEFAULTCHARSET=utf8

insertintodept(name)values("後勤部")/<code>


面試官:MySQL的可重複讀級別能解決幻讀問題嗎?

根據上面的流程執行,預期來說應該是事務1的第一條select查詢出一條數據,第二個select查詢出兩條數據(包含事務2提交的數據)。

但是在實際測試中發現第二條select實際上也只查詢處理一條數據。這是但是根據數據庫理論的可重複讀的實現(排他鎖和共享鎖)這是不應該的情況。

在瞭解實際原因前我們先複習下事務的相關理論。

數據庫原理理論

事務

事務(Transaction),一般是指要做的或所做的事情。在計算機術語中是指訪問並可能更新數據庫中各種數據項的一個程序執行單元(unit)。事務由事務開始(begin transaction)和事務結束(end transaction)之間執行的全體操作組成。

在關係數據庫中,一個事務可以是一組SQL語句或整個程序。

為什麼要有事務

一個數據庫事務通常包含對數據庫進行讀或寫的一個操作序列。它的存在包含有以下兩個目的:

  • 為數據庫操作提供了一個從失敗中恢復到正常狀態的方法,同時提供了數據庫在異常狀態下仍能保持一致性的方法。
  • 當多個應用程序在併發訪問數據庫時,可以在這些應用程序之間提供一個隔離方法,保證彼此的操作互相干擾。

事務特性

事務具有4個特性:原子性、一致性、隔離性、持久性。這四個屬性通常稱為 ACID 特性。

  • 原子性(atomicity):一個事務應該是一個不可分割的工作單位,事務中包括的操作要麼都成功,要麼都不成功。
  • 一致性(consistency):事務必須是使數據庫從一個一致性狀態變到另一個一致性狀態。一致性與原子性是密切相關的。
  • 隔離性(isolation):一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的數據在事務未提交前對併發的其他事務是隔離的,併發執行的各個事務之間不能互相影響。
  • 持久性(durability):一個事務一旦成功提交,它對數據庫中數據的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。

事務之間的幾個特性並不是一組同等的概念:

如果在任何時刻都只有一個事務,那麼其天然是具有隔離性的,這時只要保證原子性就能具有一致性。

如果存在併發的情況下,就需要保證原子性和隔離性才能保證一致性。

數據庫併發事務中存在的問題

如果不考慮事務的隔離性,會發生以下幾種問題:

髒讀

髒讀是指在一個事務處理過程裡讀取了另一個未提交的事務中的數據。當一個事務正在多次修改某個數據,而在這個事務中這多次的修改都還未提交,這時一個併發的事務來訪問該數據,就會造成兩個事務得到的數據不一致。

不可重複讀

不可重複讀是指在對於數據庫中的某條數據,一個事務範圍內多次查詢返回不同的數據值(這裡不同是指某一條或多條數據的內容前後不一致,但數據條數相同),這是由於在查詢間隔,該事務需要用到的數據被另一個事務修改並提交了。

不可重複讀和髒讀的區別是,髒讀是某一事務讀取了另一個事務未提交的髒數據,而不可重複讀則是讀取了其他事務提交的數據。需要注意的是在某些情況下不可重複讀並不是問題。

幻讀

幻讀是事務非獨立執行時發生的一種現象。例如事務T1對一個表中所有的行的某個數據項做了從“1”修改為“2”的操作,這時事務T2又對這個表中插入了一行數據項,而這個數據項的數值還是為“1”並且提交給數據庫。

而操作事務T1的用戶如果再查看剛剛修改的數據,會發現還有一行沒有修改,其實這行是從事務T2中添加的,就好像產生幻覺一樣,這就是發生了幻讀。

幻讀和不可重複讀都是讀取了另一條已經提交的事務(這點就髒讀不同),所不同的是不可重複讀可能發生在update,delete操作中,而幻讀發生在insert操作中。

排他鎖,共享鎖

排它鎖(Exclusive),又稱為X 鎖,寫鎖。

共享鎖(Shared),又稱為S 鎖,讀鎖。

讀寫鎖之間有以下的關係:

  • 一個事務對數據對象O加了 S 鎖,可以對 O進行讀取操作,但是不能進行更新操作。加鎖期間其它事務能對O 加 S 鎖,但是不能加 X 鎖。
  • 一個事務對數據對象 O 加了 X 鎖,就可以對 O 進行讀取和更新。加鎖期間其它事務不能對 O 加任何鎖。

即讀寫鎖之間的關係可以概括為:多讀單寫

事務的隔離級別

在事務中存在以下幾種隔離級別:

讀未提交(Read Uncommitted)

解決更新丟失問題。如果一個事務已經開始寫操作,那麼其他事務則不允許同時進行寫操作,但允許其他事務讀此行數據。該隔離級別可以通過“排他寫鎖”實現,即事務需要對某些數據進行修改必須對這些數據加 X 鎖,讀數據不需要加 S 鎖。

讀已提交(Read Committed)

解決了髒讀問題。讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行。這可以通過“瞬間共享讀鎖”和“排他寫鎖”實現, 即事務需要對某些數據進行修改必須對這些數據加 X 鎖,讀數據時需要加上 S 鎖,當數據讀取完成後立刻釋放 S 鎖,不用等到事務結束。

可重複讀取(Repeatable Read)

禁止不可重複讀取和髒讀取,但是有時可能出現幻讀數據。讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。

Mysql默認使用該隔離級別。這可以通過“共享讀鎖”和“排他寫鎖”實現,即事務需要對某些數據進行修改必須對這些數據加 X 鎖,讀數據時需要加上 S 鎖,當數據讀取完成並不立刻釋放 S 鎖,而是等到事務結束後再釋放。

串行化(Serializable)

解決了幻讀的問題的。提供嚴格的事務隔離。它要求事務序列化執行,事務只能一個接著一個地執行,不能併發執行。僅僅通過“行級鎖”是無法實現事務序列化的,必須通過其他機制保證新插入的數據不會被剛執行查詢操作的事務訪問到。

MySQL中的隔離級別的實現

上面的內容解釋了一些數據庫理論的概念,但是在MySQL、ORACLE這樣的數據庫中,為了性能的考慮並不是完全按照上面介紹的理論來實現的。

MVCC

多版本併發控制(Multi-Version Concurrency Control, MVCC)是MySQL中基於樂觀鎖理論實現隔離級別的方式,用於實現讀已提交和可重複讀取隔離級別的實現。

實現(隔離級別為可重複讀)

在說到如何實現前先引入兩個概念:

  • 系統版本號:一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
  • 事務版本號:事務開始時的系統版本號。

在MySQL中,會在表中每一條數據後面添加兩個字段:

  • 創建版本號:創建一行數據時,將當前系統版本號作為創建版本號賦值
  • 刪除版本號:刪除一行數據時,將當前系統版本號作為刪除版本號賦值

SELECT

select時讀取數據的規則為:創建版本號<=當前事務版本號,刪除版本號為空或>當前事務版本號。

創建版本號<=當前事務版本號保證取出的數據不會有後啟動的事務中創建的數據。這也是為什麼在開始的示例中我們不會查出後來添加的數據的原因

刪除版本號為空或>當前事務版本號保證了至少在該事務開啟之前數據沒有被刪除,是應該被查出來的數據。

INSERT

insert是將當前的系統版本號賦值給創建版本號字段。

UPDATE

插入一條新記錄,保存當前事務版本號為行創建版本號,同時保存當前事務版本號到原來刪除的行,實際上這裡的更新是通過delete和insert實現的。

DELETE

刪除時將當前的系統版本號賦值給刪除版本號字段,標識該行數據在那一個事務中會被刪除,即使實際上在位commit時該數據沒有被刪除。根據select的規則後開啟的數據也不會查詢到該數據。

MVCC真的解決了幻讀?

從最開始我們的測試示例和上面的理論支持來看貌似在MySQL中通過MVCC就解決了幻讀的問題,那既然這樣串行化讀貌似就沒啥意義了,帶著疑問繼續測試。

測試前數據:

面試官:MySQL的可重複讀級別能解決幻讀問題嗎?


面試官:MySQL的可重複讀級別能解決幻讀問題嗎?

根據上面的結果我們期望的結果是這樣的:

面試官:MySQL的可重複讀級別能解決幻讀問題嗎?

但是實際上我們的經過是:

面試官:MySQL的可重複讀級別能解決幻讀問題嗎?

本來我們希望得到的結果只是第一條數據的部門改為財務,但是結果確實兩條數據都被修改了。

這種結果告訴我們其實在MySQL可重複讀的隔離級別中並不是完全解決了幻讀的問題,而是解決了讀數據情況下的幻讀問題。而對於修改的操作依舊存在幻讀問題,就是說MVCC對於幻讀的解決時不徹底的。

快照讀和當前讀

出現了上面的情況我們需要知道為什麼會出現這種情況。在查閱了一些資料後發現在RR級別中,通過MVCC機制,雖然讓數據變得可重複讀,但我們讀到的數據可能是歷史數據,不是數據庫最新的數據。

這種讀取歷史數據的方式,我們叫它快照讀 (snapshot read),而讀取數據庫最新版本數據的方式,叫當前讀 (current read)。

select 快照讀

當執行select操作是innodb默認會執行快照讀,會記錄下這次select後的結果,之後select 的時候就會返回這次快照的數據,即使其他事務提交了不會影響當前select的數據,這就實現了可重複讀了。

快照的生成當在第一次執行select的時候,也就是說假設當A開啟了事務,然後沒有執行任何操作,這時候B insert了一條數據然後commit,這時候A執行 select,那麼返回的數據中就會有B添加的那條數據。

之後無論再有其他事務commit都沒有關係,因為快照已經生成了,後面的select都是根據快照來的。

當前讀

對於會對數據修改的操作(update、insert、delete)都是採用當前讀的模式。在執行這幾個操作時會讀取最新的記錄,即使是別的事務提交的數據也可以查詢到。

假設要update一條記錄,但是在另一個事務中已經delete掉這條數據並且commit了,如果update就會產生衝突,所以在update的時候需要知道最新的數據。也正是因為這樣所以才導致上面我們測試的那種情況。

select的當前讀需要手動的加鎖:

<code>select*fromtablewhere?lockinsharemode;
select*fromtablewhere?forupdate;
/<code>

有個問題說明下

在測試過程中最開始我以為使用begin語句就是開始一個事務了,所以在上面第二次測試中因為先開始的事務1,結果在事務1中卻查到了事務2新增的數據,當時認為這和前面MVCC中的select的規則不一致了,所以做了如下測試:

面試官:MySQL的可重複讀級別能解決幻讀問題嗎?

<code>SELECT*FROMinformation_schema.INNODB_TRX//用於查詢當前正在執行中的事務
/<code>

可以看到如果只是執行begin語句實際上並沒有開啟一個事務。

下面在begin後添加一條select語句:

面試官:MySQL的可重複讀級別能解決幻讀問題嗎?

所以要明白實際上是對數據進行了增刪改查等操作後才開啟了一個事務。

如何解決幻讀

很明顯可重複讀的隔離級別沒有辦法徹底的解決幻讀的問題,如果我們的項目中需要解決幻讀的話也有兩個辦法:

  • 使用串行化讀的隔離級別
  • MVCC+next-key locks:next-key locks由record locks(索引加鎖) 和 gap locks(間隙鎖,每次鎖住的不光是需要使用的數據,還會鎖住這些數據附近的數據)

實際上很多的項目中是不會使用到上面的兩種方法的,串行化讀的性能太差,而且其實幻讀很多時候是我們完全可以接受的。


分享到:


相關文章: