1. 程式人生 > >數據庫原理-事務隔離與多版本並發控制(MVCC)

數據庫原理-事務隔離與多版本並發控制(MVCC)

機制 日誌記錄 write 必須 bili 很好 隔離級別 類型 讀寫鎖

剛來美團實習,正好是星期天,不得不說,其內部的資料很豐富,看了部分文檔後,對數據庫事務這塊更理解了。數據庫事務的ACID,大家都知道,為了維護這些性質,主要是隔離性和一致性,一般使用加鎖這種方式。同時數據庫又是個高並發的應用,同一時間會有大量的並發訪問,如果加鎖過度,會極大的降低並發處理能力。所以對於加鎖的處理,可以說就是數據庫對於事務處理的精髓所在。在數據實現隔離級別時候,用到了一種多版本並發控制的技術,具體實現方式可能由引擎決定,本文主要對它的原理進行講解。

由基本原理談起

所熟知的

技術分享圖片


事務指的是滿足 ACID 特性的一組操作,可以通過 Commit 提交一個事務,也可以使用 Rollback 進行回滾。

ACID

  • 原子性(Atomicity)

事務被視為不可分割的最小單元,事務的所有操作要麽全部提交成功,要麽全部失敗回滾。回滾可以用日誌來實現,日誌記錄著事務所執行的修改操作,在回滾時反向執行這些修改操作即可。

  • 一致性(Consistency)

數據庫在事務執行前後都保持一致性狀態。在一致性狀態下,所有事務對一個數據的讀取結果都是相同的。

  • 隔離性(Isolation)

一個事務所做的修改在最終提交以前,對其它事務是不可見的。

  • 持久性(Durability)

一旦事務提交,則其所做的修改將會永遠保存到數據庫中。即使系統發生崩潰,事務執行的結果也不能丟失。可以通過數據庫備份和恢復來實現,在系統發生奔潰時,使用備份的數據庫進行數據恢復。


事務的 ACID 特性概念簡單,但不是很好理解,主要是因為這幾個特性不是一種平級關系:

  • 只有滿足一致性,事務的執行結果才是正確的。
  • 在無並發的情況下,事務串行執行,隔離性一定能夠滿足。此時要只要能滿足原子性,就一定能滿足一致性。
  • 在並發的情況下,多個事務並發執行,事務不僅要滿足原子性,還需要滿足隔離性,才能滿足一致性。
  • 事務滿足持久化是為了能應對數據庫奔潰的情況。
技術分享圖片


AUTOCOMMIT

MySQL 默認采用自動提交模式。也就是說,如果不顯式使用START TRANSACTION語句來開始一個事務,那麽每個查詢都會被當做一個事務自動提交。

並發一致性問題

在並發環境下,事務的隔離性很難保證,因此會出現很多並發一致性問題。

丟失修改

T1 和 T2 兩個事務都對一個數據進行修改,T1 先修改,T2 隨後修改,T2 的修改覆蓋了 T1 的修改。

技術分享圖片


讀臟數據

T1 修改一個數據,T2 隨後讀取這個數據。如果 T1 撤銷了這次修改,那麽 T2 讀取的數據是臟數據。

技術分享圖片


不可重復讀

T2 讀取一個數據,T1 對該數據做了修改。如果 T2 再次讀取這個數據,此時讀取的結果和第一次讀取的結果不同。

技術分享圖片


幻影讀

T1 讀取某個範圍的數據,T2 在這個範圍內插入新的數據,T1 再次讀取這個範圍的數據,此時讀取的結果和和第一次讀取的結果不同。

技術分享圖片



產生並發不一致性問題主要原因是破壞了事務的隔離性,解決方法是通過並發控制來保證隔離性。並發控制可以通過封鎖來實現,但是封鎖操作需要用戶自己控制,相當復雜。數據庫管理系統提供了事務的隔離級別,讓用戶以一種更輕松的方式處理並發一致性問題。

封鎖

封鎖粒度

技術分享圖片


MySQL 中提供了兩種封鎖粒度:行級鎖以及表級鎖。

應該盡量只鎖定需要修改的那部分數據,而不是所有的資源。鎖定的數據量越少,發生鎖爭用的可能就越小,系統的並發程度就越高。

但是加鎖需要消耗資源,鎖的各種操作(包括獲取鎖、釋放鎖、以及檢查鎖狀態)都會增加系統開銷。因此封鎖粒度越小,系統開銷就越大。

在選擇封鎖粒度時,需要在鎖開銷和並發程度之間做一個權衡。

封鎖類型

1. 讀寫鎖

  • 排它鎖(Exclusive),簡寫為 X 鎖,又稱寫鎖。
  • 共享鎖(Shared),簡寫為 S 鎖,又稱讀鎖。

有以下兩個規定:

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

鎖的兼容關系如下:

- X S
X NO NO
S NO YES

2. 意向鎖

使用意向鎖(Intention Locks)可以更容易地支持多粒度封鎖。

在存在行級鎖和表級鎖的情況下,事務 T 想要對表 A 加 X 鎖,就需要先檢測是否有其它事務對表 A 或者表 A 中的任意一行加了鎖,那麽就需要對表 A 的每一行都檢測一次,這是非常耗時的。

意向鎖在原來的 X/S 鎖之上引入了 IX/IS,IX/IS 都是表鎖,用來表示一個事務想要在表中的某個數據行上加 X 鎖或 S 鎖。有以下兩個規定:

  • 一個事務在獲得某個數據行對象的 S 鎖之前,必須先獲得表的 IS 鎖或者更強的鎖;
  • 一個事務在獲得某個數據行對象的 X 鎖之前,必須先獲得表的 IX 鎖。

通過引入意向鎖,事務 T 想要對表 A 加 X 鎖,只需要先檢測是否有其它事務對表 A 加了 X/IX/S/IS 鎖,如果加了就表示有其它事務正在使用這個表或者表中某一行的鎖,因此事務 T 加 X 鎖失敗。

各種鎖的兼容關系如下:

- X IX S IS
X NO NO NO NO
IX NO YES NO YES
S NO NO YES YES
IS NO YES YES YES

解釋如下:

  • 任意 IS/IX 鎖之間都是兼容的,因為它們只是表示想要對表加鎖,而不是真正加鎖;
  • S 鎖只與 S 鎖和 IS 鎖兼容,也就是說事務 T 想要對數據行加 S 鎖,其它事務可以已經獲得對表或者表中的行的 S 鎖。

封鎖協議

1. 三級封鎖協議

一級封鎖協議

事務 T 要修改數據 A 時必須加 X 鎖,直到 T 結束才釋放鎖。

可以解決丟失修改問題,因為不能同時有兩個事務對同一個數據進行修改,那麽事務的修改就不會被覆蓋。

T1 T2
lock-x(A)
read A=20
lock-x(A)
wait
write A=19 .
commit .
unlock-x(A) .
obtain
read A=19
write A=21
commit
unlock-x(A)

二級封鎖協議

在一級的基礎上,要求讀取數據 A 時必須加 S 鎖,讀取完馬上釋放 S 鎖。

可以解決讀臟數據問題,因為如果一個事務在對數據 A 進行修改,根據 1 級封鎖協議,會加 X 鎖,那麽就不能再加 S 鎖了,也就是不會讀入數據。

T1 T2
lock-x(A)
read A=20
write A=19
lock-s(A)
wait
rollback .
A=20 .
unlock-x(A) .
obtain
read A=20
commit
unlock-s(A)

三級封鎖協議

在二級的基礎上,要求讀取數據 A 時必須加 S 鎖,直到事務結束了才能釋放 S 鎖。

可以解決不可重復讀的問題,因為讀 A 時,其它事務不能對 A 加 X 鎖,從而避免了在讀的期間數據發生改變。

T1 T2
lock-s(A)
read A=20
lock-x(A)
wait
read A=20 .
commit .
unlock-s(A) .
obtain
read A=20
write A=19
commit
unlock-X(A)

2. 兩段鎖協議

加鎖和解鎖分為兩個階段進行。

可串行化調度是指,通過並發控制,使得並發執行的事務結果與某個串行執行的事務結果相同。

事務遵循兩段鎖協議是保證可串行化調度的充分條件。例如以下操作滿足兩段鎖協議,它是可串行化調度。

lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B)

但不是必要條件,例如以下操作不滿足兩段鎖協議,但是它還是可串行化調度。

lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)

MySQL 隱式與顯示鎖定

MySQL 的 InnoDB 存儲引擎采用兩段鎖協議,會根據隔離級別在需要的時候自動加鎖,並且所有的鎖都是在同一時刻被釋放,這被稱為隱式鎖定。

InnoDB 也可以使用特定的語句進行顯示鎖定:

SELECT ... LOCK In SHARE MODE;
SELECT ... FOR UPDATE;

隔離級別

未提交讀(READ UNCOMMITTED)

事務中的修改,即使沒有提交,對其它事務也是可見的。

提交讀(READ COMMITTED)

一個事務只能讀取已經提交的事務所做的修改。換句話說,一個事務所做的修改在提交之前對其它事務是不可見的。

可重復讀(REPEATABLE READ)

保證在同一個事務中多次讀取同樣數據的結果是一樣的。

可串行化(SERIALIZABLE)

強制事務串行執行。


隔離級別 臟讀 不可重復讀 幻影讀
未提交讀 YES YES YES
提交讀 NO YES YES
可重復讀 NO NO YES
可串行化 NO NO NO

多版本並發控制

重點終於來了,多版本並發控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存儲引擎實現隔離級別的一種具體方式,用於實現提交讀和可重復讀這兩種隔離級別。而未提交讀隔離級別總是讀取最新的數據行,無需使用 MVCC;可串行化隔離級別需要對所有讀取的行都加鎖,單純使用 MVCC 無法實現。

版本號

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

InooDB 的 MVCC 在每行記錄後面都保存著兩個隱藏的列,用來存儲兩個版本號:

  • 創建版本號:指示創建一個數據行的快照時的系統版本號;
  • 刪除版本號:如果該快照的刪除版本號大於當前事務版本號表示該快照有效,否則表示該快照已經被刪除了。

Undo 日誌

InnoDB 的 MVCC 使用到的快照存儲在 Undo 日誌中,該日誌通過回滾指針把一個數據行(Record)的所有快照連接起來。

技術分享圖片


實現過程

以下實現過程針對可重復讀隔離級別。

1. SELECT

當開始新一個事務時,該事務的版本號肯定會大於當前所有數據行快照的創建版本號,理解這一點很關鍵。

多個事務必須讀取到同一個數據行的快照,並且這個快照是距離現在最近的一個有效快照。但是也有例外,如果有一個事務正在修改該數據行,那麽它可以讀取事務本身所做的修改,而不用和其它事務的讀取結果一致。

把沒有對一個數據行做修改的事務稱為 T,T 所要讀取的數據行快照的創建版本號必須小於 T 的版本號,因為如果大於或者等於 T 的版本號,那麽表示該數據行快照是其它事務的最新修改,因此不能去讀取它。

除了上面的要求,T 所要讀取的數據行快照的刪除版本號必須大於 T 的版本號,因為如果小於等於 T 的版本號,那麽表示該數據行快照是已經被刪除的,不應該去讀取它。

2. INSERT

將當前系統版本號作為數據行快照的創建版本號。

3. DELETE

將當前系統版本號作為數據行快照的刪除版本號。

4. UPDATE

將當前系統版本號作為更新前的數據行快照的刪除版本號,並將當前系統版本號作為更新後的數據行快照的創建版本號。可以理解為先執行 DELETE 後執行 INSERT。

快照讀與當前讀

1. 快照讀

使用 MVCC 讀取的是快照中的數據,這樣可以減少加鎖所帶來的開銷。

select * from table ...;

2. 當前讀

讀取的是最新的數據,需要加鎖。以下第一個語句需要加 S 鎖,其它都需要加 X 鎖。

select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update;
delete;

寫在最後

或許還是一頓蒙,MVCC的本質是記錄下修改,將修改記錄用指針串聯起來,最後利用四種版本號(系統版本號,事務版本號,創建版本號,刪除版本號)實現數據庫的增刪改查的機制,通過這種方式只能保證可重復讀和提交讀,至於實際情況呢,在參考1中博主寫的更透徹,可以看看,另外幻讀的問題,也就是Next-key-lock,另外再寫吧。

參考

  • 事務隔離級別與鎖的關系
  • Mysql與Innodb

數據庫原理-事務隔離與多版本並發控制(MVCC)