1. 程式人生 > >MySQL實戰 | 03 - 誰動了我的資料:淺析MySQL的事務隔離級別

MySQL實戰 | 03 - 誰動了我的資料:淺析MySQL的事務隔離級別

原文連結:這一次,帶你搞清楚MySQL的事務隔離級別!

使用過關係型資料庫的,應該都事務的概念有所瞭解,知道事務有 ACID 四個基本屬性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和永續性(Durability),今天我們主要來理解一下事務的隔離性。

宣告:MySQL專欄學習系列,基本上是本人學習極客時間《MySQL實戰45講》專欄內容的筆記,並在專欄基礎上進行知識點挖掘。侵刪。
本人也不是什麼 DBA,所以有些錯誤的地方請大家指正,相互交流,共同進步!

什麼是事務?

資料庫事務(簡稱:事務)是資料庫管理系統執行過程中的一個邏輯單位,由一個有限的資料庫操作序列構成。—— 維基百科

事務的概念看上去不難,但是需要注意以下幾個點:

1、首先,事務就是要保證一組資料庫操作,要麼全部成功,要麼全部失敗;

2、在 MySQL 中,事務支援是在引擎層實現的;

3、並不是所有引擎都支援事務,如 MyISAM 就不支援,InnoDB 就支援;


今天,我們的主角是隔離性,隔離性是指當多個使用者併發操作資料庫時,資料庫為每一個使用者開啟不同的事務,這些事務之間相互不干擾,相互隔離。

為什麼需要隔離性?

如果事務之間不是互相隔離的,可能將會出現以下問題。

1、髒讀

髒讀(dirty read),簡單來說,就是一個事務在處理過程中讀取了另外一個事務未提交的資料。

這種未提交的資料我們稱之為髒資料。依據髒資料所做的操作肯能是不正確的。

還記得上節中我們提到的 dirty page 嗎?這種臨時處理的未提交的,都是「髒」的。

舉例

時間點 事務A 事務B
1 開啟事務A
2 開啟事務B
3 查詢餘額為100
4 餘額增加至150
5 查詢餘額為150

比如,你給小編讚賞 1 分錢,整個事務需要兩個步驟:
①給小編賬號加一分錢,這時小編看到了,覺得很欣慰;
②你的賬號減一分錢;

但是,若該事務未提交成功,最終所有操作都會回滾,小編看到的一分錢也只是鏡花水月。

傷心

2、不可重複讀

不可重複讀(non-repeatable read),是指一個事務範圍內,多次查詢某個資料,卻得到不同的結果。

在第一個事務中的兩次讀取資料之間,由於第二個事務的修改,第一個事務兩次讀到的資料可能就是不一樣的。

舉例

時間點 事務A 事務B
1 開啟事務A
2 開啟事務B
3 查詢餘額為100
4 餘額增加至150
5 查詢餘額為100
6 提交事務
7 查詢餘額為150

接著上一個例子,假設你真給小編打賞了一分錢,小編樂得屁顛屁顛地去準備提現,一查,發現真多了一分錢。

在這同時,在我還沒有提現成功之前,小編的老婆已經提前將這一分錢支走了,小編此時再次查賬,發現一分錢也沒了。

再次大哭

髒讀和不可重複讀有點懵逼?

二者的區別是,髒讀是某一事務讀取了另外一個事務未提交的資料,不可重複讀是讀取了其他事務提交的資料。

其實,有些情況下,不可重複讀不是問題,比如,小編提現期間,一分錢被老婆支走了,這不是問題!

而髒讀,是可以通過設定隔離級別避免的。

3、幻讀

幻讀(phantom read),是事務非獨立執行時發生的一種現象。

例如事務 T1 對一個表中所有的行的某個資料項做了從“1”修改為“2”的操作,這時事務 T2 又對這個表中插入了一行資料項為“1”的資料,並且提交給資料庫。

而操作事務 T1 的使用者如果再檢視剛剛修改的資料,會發現資料怎麼還是 1?其實這行是從事務 T2 中新增的,就好像產生幻覺一樣,這就是發生了幻讀。

舉例

時間點 事務A 事務B
1 開啟事務A
2 開啟事務B
3 查詢id<3的所有記錄,共3條
4 插入一條記錄id=2
5 提交事務
6 查詢id<3的所有記錄,共4條

其實上面的解釋已經是一個例子了,但是還是要舉個例子。

比如,小編準備提取你打賞的一分錢,提取完了,這時又有其他熱心網友打賞了一分錢,小編一看,明明已經取出了,怎麼又有一分錢!?

小編此時以為像做夢一樣,我覺得也可以叫「夢讀」,哈哈。

幻讀和不可重複讀都是讀取了另一條已經提交的事務(這點就髒讀不同),所不同的是不可重複讀查詢的都是同一個資料項,而幻讀針對的是一批資料整體(比如資料的個數)。

事務的隔離級別

為了解決上面可能出現的問題,我們就需要設定隔離級別,也就是事務之間按照什麼規則進行隔離,將事務隔離到什麼程度。

首先,需要明白一點,隔離程度越強,事務的執行效率越低。

ANSI/ISO SQL 定義了 4 種標準隔離級別:

① Serializable(序列化):花費最高代價但最可靠的事務隔離級別。

“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。

事務 100% 隔離,可避免髒讀、不可重複讀、幻讀的發生。

② Repeatable read(可重複讀,預設級別):多次讀取同一範圍的資料會返回第一次查詢的快照,即使其他事務對該資料做了更新修改。事務在執行期間看到的資料前後必須是一致的。

但如果這個事務在讀取某個範圍內的記錄時,其他事務又在該範圍內插入了新的記錄,當之前的事務再次讀取該範圍的記錄時,會產生幻行,這就是幻讀。

可避免髒讀、不可重複讀的發生。但是可能會出現幻讀。

③ Read committed (讀已提交):保證一個事物提交後才能被另外一個事務讀取。另外一個事務不能讀取該事物未提交的資料。

可避免髒讀的發生,但是可能會造成不可重複讀。

大多數資料庫的預設級別就是 Read committed,比如 Sql Server , Oracle。

④ Read uncommitted (讀未提交):最低的事務隔離級別,一個事務還沒提交時,它做的變更就能被別的事務看到。

任何情況都無法保證。

隔離級別

下圖中是一個很好的例子,分別解釋了四種事務隔離級別下,事務 B 能夠讀取到的結果。

示例


看著還是有點懵逼?那我們再舉個例子。

A,B 兩個事務,分別做了一些操作,操作過程中,在不同隔離級別下檢視變數的值:

|:-:|:-:|:-:|:-:|:-:|:-:|
|啟動事務,查詢變數V的值為1|啟動事務|||||
||查詢V的值為1|||||
||將V的值修改為2|||||
|查詢V的值||2|1|1|1|
||提交事務B||||
|查詢V的值||2|2|1|1|
|提交事務A||||||
|查詢V的值||2|2|2|2|

隔離級別是序列化,則在事務 B 執行「將 1 改成 2」的時候,會被鎖住。直到事務 A 提交後,事務 B 才可以繼續執行。

再次總結

讀未提交:別人改資料的事務尚未提交,我在我的事務中也能讀到。
讀已提交:別人改資料的事務已經提交,我在我的事務中才能讀到。
可重複讀:別人改資料的事務已經提交,我在我的事務中也不去讀。
序列:我的事務尚未提交,別人就別想改資料。

這 4 種隔離級別,並行效能依次降低,安全性依次提高。

總的來說,事務隔離級別越高,越能保證資料的完整性和一致性,但是付出的代價卻是併發執行效率的低下。


隔離級別的實現

事務的機制是通過檢視(read-view)來實現的併發版本控制(MVCC),不同的事務隔離級別建立讀檢視的時間點不同。

  • 可重複讀是每個事務重建讀檢視,整個事務存在期間都用這個檢視。
  • 讀已提交是每條 SQL 建立讀檢視,在每個 SQL 語句開始執行的時候建立的。隔離作用域僅限該條 SQL 語句。
  • 讀未提交是不建立,直接返回記錄上的最新值
  • 序列化隔離級別下直接用加鎖的方式來避免並行訪問。

這裡的檢視可以理解為資料副本,每次建立檢視時,將當前已持久化的資料建立副本,後續直接從副本讀取,從而達到資料隔離效果。

隔離級別的實現

我們每一次的修改操作,並不是直接對行資料進行操作。

比如我們設定 id 為 3 的行的 A 屬性為 10,並不是直接修改表中的資料,而是新加一行。

同時資料表其實還有一些隱藏的屬性,比如每一行的事務 id,所以每一行資料可能會有多個版本,每一個修改過它的事務都會有一行,並且還會有關聯的 undo 日誌,表示這個操作原來的資料是什麼,可以用它做回滾。

那麼為什麼要這麼做?

因為如果我們直接把資料修改了,那麼其他事務就用不了原先的值了,違反了事務的一致性。

那麼一個事務讀取某一行的資料到底返回什麼結果呢?

取決於隔離級別,如果是 Read Committed,那麼返回的是最新的事務的提交值,所以未提交的事務修改的值是不會讀到的,這就是 Read Committed 實現的原理。

如果是 Read Repeatable 級別,那麼只能返回發起時間比當前事務早的事務的提交值,和比當前事務晚的刪除事務刪除的值。這其實就是 MVCC 方式。

undo log

undo log 中儲存的是老版本資料。假設修改表中 id=2 的行資料,把 Name='B' 修改為 Name = 'B2' ,那麼 undo 日誌就會用來存放 Name='B' 的記錄,如果這個修改出現異常,可以使用 undo 日誌來實現回滾操作,保證事務的一致性。

當一箇舊的事務需要讀取資料時,為了能讀取到老版本的資料,需要順著 undo 鏈找到滿足其可見性的記錄。當版本鏈很長時,通常可以認為這是個比較耗時的操作。

假設一個值從 1 被按順序改成了 2、3、4,在回滾日誌裡面就會有類似下面的記錄。

回滾日誌

當前值是 4,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的 read-view。

如圖中看到的,在檢視 A、B、C 裡面,這一個記錄的值分別是 1、2、4,同一條記錄在系統中可以存在多個版本,就是資料庫的多版本併發控制(MVCC)。對於 read-view A,要得到 1,就必須將當前值依次執行圖中所有的回滾操作得到。

同時你會發現,即使現在有另外一個事務正在將 4 改成 5,這個事務跟 read-view A、B、C 對應的事務是不會衝突的。


另外,在回滾段中的 undo log 分為: insert undo log 和 update undo log:

  • insert undo log : 事務對 insert 新記錄時產生的 undolog,只在事務回滾時需要,並且在事務提交後就可以立即丟棄。(誰會對剛插入的資料有可見性需求呢!!)
  • update undo log : 事務對記錄進行 delete 和 update 操作時產生的 undo log。不僅在事務回滾時需要,一致性讀也需要,所以不能隨便刪除,只有當資料庫所使用的快照中不涉及該日誌記錄,對應的回滾日誌才會被 purge 執行緒刪除。

何時刪除?

在不需要的時候才刪除。也就是說,系統會判斷,當沒有事務再需要用到這些回滾日誌時,回滾日誌會被刪除。

就是當系統裡沒有比這個回滾日誌更早的 read-view 的時候。

長事務

直觀感覺,一個事務花費很長時間不能夠結束,就是一個長的事務,簡稱長事務(Long Transaction)。

長事務是資料庫使用者經常會碰到且是非常令人頭疼的問題。長事務處理需要恰當進行,如處理不當可能引起資料庫的崩潰,為使用者帶來不必要的損失。

根據上面的論述,長事務意味著系統裡面會存在很老的事務檢視。

由於這些事務隨時可能訪問資料庫裡面的任何資料,所以這個事務提交之前,資料庫裡面它可能用到的 undo log 都必須保留,這就會導致大量佔用儲存空間。

在 MySQL 5.5 及以前的版本,回滾日誌是跟資料字典一起放在 ibdata 檔案裡的,即使長事務最終提交,回滾段被清理,檔案也不會變小

除了對回滾段的影響,長事務還佔用鎖資源,也可能拖垮整個庫,這個我們會在後面講鎖的時候展開。

因此,我們要儘量避免長事務。

小結

這一節主要是事務的隔離級別,主要需要記住幾個隔離級別、瞭解一下實現方式。

感覺東西有點亂,涉及了 MVCC 的東西,作者也沒有展開,我能力有限,也就沒有再深挖。後續,作者在涉及相關知識點時,我們再進行探討。