深入淺出MYSQL的事務隔離

coding丁發表於2020-09-26

深入淺出MYSQL的事務隔離

Innodb 資料庫引擎的資料庫或表支援事務,MyISAM不支援事務,這也是MYSQL預設引擎換成Innodb的重要原因。MySQL 事務主要用於處理操作量大,複雜度高的資料,事務處理可以用來維護資料庫的完整性,保證成批的 SQL 語句要麼全部執行,要麼全部不執行。

ACID–事務四大特徵

事務是由一組SQL語句組成的邏輯處理單元,具有4個屬性,通常簡稱為事務的ACID屬性。
在這裡插入圖片描述

  • 原子性 Atomicity:整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
  • 一致性 Consistency:在事務開始之前和事務結束以後,資料庫的完整性約束沒有被破壞。
  • 隔離性 Isolation:一個事務的執行不能其它事務干擾。即一個事務內部的操作及使用的資料對其它併發事務是隔離的,併發執行的各個事務之間不能互相干擾,隔離性可以防止多個事務併發執行時由於交叉執行而導致資料的不一致。
  • 永續性 Durability:在事務完成以後,該事務所對資料庫所作的更改便持久的儲存在資料庫之中,即便系統發生故障也不會丟失。

事務隔離

多個事務併發處理時,會帶來諸多問題:

  • 更新丟失:A和B事務併發執行,A事務執行更新後,提交;B事務在A事務更新後,B事務結束前也做了對該行資料的更新操作,然後回滾,則兩次更新操作都丟失了
  • 髒讀:事務A讀取了事務B更新的資料,然後B回滾操作,那麼A讀取到的資料是髒資料
  • 不可重複讀:事務 A 多次讀取同一資料,事務B在事務A多次讀取的過程中,對資料作了更新並提交,導致事務A多次讀取同一資料時,結果不一致。不可重複讀的重點是修改:在同一事務中,同樣的條件,第一次讀的資料和第二次讀的資料不一樣
  • 幻讀:一個事務A讀取了幾行資料,接著另一個併發事務B插入了一些資料時。在隨後的查詢中,事務A就會發現多了一些原本不存在的記錄,就好像發生了幻覺一樣,所以稱為幻讀。幻讀的重點在於新增或者刪除:在同一事務中,同樣的條件,,第一次和第二次讀出來的記錄數不一樣

髒讀是指讀取了未修改完的記錄,不可重複讀指因為被其它事務修改了記錄導致某事務兩次讀取記錄不一致,而幻讀是指因為其它事務對錶做了增刪導致某事務兩次讀取的表記錄數不一致問題

為了解決併發事務的問題,就有了資料庫事務的隔離級別。首先需要明確的是:隔離越嚴格,效率就會越低

SQL 標準的事務隔離級別包括:讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和序列化(serializable )

  • 讀未提交是指,一個事務還沒提交時,它做的變更就能被別的事務看到。

  • 讀提交是指,一個事務提交之後,它做的變更才會被其他事務看到。

  • 可重複讀是指,一個事務執行過程中看到的資料,總是跟這個事務在啟動時看到的資料是一致的。當然在可重複讀隔離級別下,未提交變更對其他事務也是不可見的。InnoDB預設是可重複讀級別的

  • 序列化,顧名思義是對於同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。

隔離級別髒讀(Dirty Read)不可重複讀(NonRepeatable Read)幻讀(Phantom Read)
未提交讀(Read uncommitted)可能可能可能
已提交讀(Read committed)不可能可能可能
可重複讀(Repeatable read)不可能不可能可能
可序列化(Serializable )不可能不可能不可能

用例子說明幾種不同的隔離級別:

讀未提交:

一個事務可以讀取另一個未提交事務的資料。

例子:小A給小B轉賬,轉賬時不小心按錯了數字,本來轉100呢結果轉了1000,不過這是一個事件,還沒有提交,所以小A趕緊回滾了該事務,然後又轉了100。但是因為是讀未提交的隔離級別,所以雖然事件還沒有commit,但是小B看到了你只轉去了1000,然後過了一會發現怎麼最後轉的是100。

也就是說,該事件還沒提交,但是別的事務可以看到它的未提交的資料,也就是讀了髒資料。

讀已提交:

一個事務只能讀取另一個事務已經提交的資料,可以避免髒資料。

例子:小A卡里有1000塊,在消費時,小A的老婆也用了他的卡買了東西,由於是讀已提交,因此雖然小A的老婆已經付過錢,但是此時還沒有提交,所以小A看到的仍然是1000塊。但是小A付賬前,小A老婆完成了提交,此時小A付帳前檢視餘額發現餘額已經不足,支付失敗,非常鬱悶。

讀已提交解決了髒資料的問題,但是沒有解決不可重複讀的問題,即一個事務的相同查詢返回不同的資料。

可重複讀:

一個事務不一定能夠立馬讀到另一個事務已經提交的資料,事務內的對同一資料的多次讀取應該是一致的。

例子:小A卡里有1000塊,小A的事件先讀取了餘額,小A的老婆想用了他的卡買東西,由於小A的事件已經讀取了餘額,因此在該事件提交前,小A的老婆不能對該記錄進行修改了,因此小A的老婆無法完成提交。

可重複讀解決了髒讀和不可重複讀的問題,但仍有出現幻讀的可能。

序列化:

序列化是最高的事務隔離級別,在該級別下,事務序列化順序執行,可以避免髒讀、不可重複讀與幻讀。簡單來說,序列化會在讀取的每一行資料上都加鎖,所以可能導致大量的超時和鎖爭用問題。這種事務隔離級別效率低下,比較耗資料庫效能,一般不使用。

不同隔離級別

例子:

一個很簡單的資料表,只有一行資料,該資料值為1,兩個事務按如下方式進行:

事務A事務B
開始事務
開始事務
查詢得到該資料值為1
查詢得到該資料值為1
將1改為2
查詢得到該資料值為X
提交事務
查詢得到該資料值為Y
提交事務
查詢得到該資料值為Z

在不同的隔離級別下,事務 A 會有哪些不同的返回結果:

  • 若隔離級別是“讀未提交”,則雖然事務B修改1為2後沒有提交,但是A已經可以讀取到該修改,所以事務A的X、Y、Z值均為2.
  • 若隔離級別是“讀提交”,則雖然事務B已經修改了該值,但是提交前事務A是不可看見的,所以X的值應該是1,Y和Z的值是2
  • 若隔離級別是“可重複讀”,則事務A在執行期間看到的資料必須保持一致的,因此X和Y都是1,Z的值是2
  • 若隔離級別是“序列化”,則在事務 B 執行“將 1 改成 2”的時候,會被鎖住,因為事務A正在讀取該資料,不允許對該資料進行修改的,所以直到事務 A 提交後,事務 B 才可以繼續執行,也就是說真正的執行順序會發生一些變化。因此X和Y的值是1,Z的值是2

在實現上,資料庫裡面會建立一個檢視,訪問的時候以檢視的邏輯結果為準。在“可重複讀”隔離級別下,這個檢視是在事務啟動時建立的,整個事務存在期間都用這個檢視。在“讀提交”隔離級別下,這個檢視是在每個 SQL 語句開始執行的時候建立的。這裡需要注意的是,“讀未提交”隔離級別下直接返回記錄上的最新值,沒有檢視概念;而“序列化”隔離級別下直接用加鎖的方式來避免並行訪問。

事務隔離的實現

在 MySQL 中,實際上每條記錄在更新的時候都會同時記錄一條回滾操作。記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值。

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

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

通過回滾日誌,用於實現一致性。系統會在該回滾日誌不再需要的時候才刪除。也就是說,系統會判斷,當沒有事務再需要用到這些回滾日誌時,回滾日誌會被刪除,也就是當系統裡沒有比這個回滾日誌更早的 read-view 時。

所以儘量避免使用長事務,長事務意味著系統裡面會存在很老的事務檢視。由於這些事務隨時可能訪問資料庫裡面的任何資料,所以這個事務提交之前,資料庫裡面它可能用到的回滾記錄都必須保留,這就會導致大量佔用儲存空間,同時,長事務還佔用鎖資源,對資料庫效能影響比較大。

InnoDB 的行資料有多個版本,每個資料版本有自己的 row trx_id,每個事務或者語句有自己的一致性檢視。普通查詢語句是一致性讀,一致性讀會根據 row trx_id 和一致性檢視確定資料版本的可見性。對於可重複讀,查詢只承認在事務啟動前就已經提交完成的資料;對於讀提交,查詢只承認在語句啟動前就已經提交完成的資料;而當前讀,總是讀取已經提交完成的最新版本。

相關文章