MySQL總結之InnoDB事務實現

我們都知道事務的幾種性質,數據庫爲了維護這些性質,尤其是一致性和隔離性,一般使用加鎖這種方式。

同時數據庫又是個高併發的應用,同一時間會有大量的併發訪問,如果加鎖過度,會極大的降低併發處理能力。

所以對於加鎖的處理,可以說就是數據庫對於事務處理的精髓所在。

這裏通過分析MySQL中InnoDB引擎的事務處理來理解事務、鎖、隔離級別、MVCC、Next-Key Locks等概念。

事務

概念

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

ACID

原子性(Atomicity)

事務被視爲不可分割的最小單元,事務的所有操作要麼全部提交成功,要麼全部失敗回滾。

回滾可以用回滾日誌來實現,回滾日誌記錄着事務所執行的修改操作,在回滾時反向執行這些修改操作即可。

一致性(Consistency)

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

隔離性(Isolation)

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

持久性(Durability)

一旦事務提交,則其所做的修改將會永遠保存到數據庫中。即使系統發生崩潰,事務執行的結果也不能丟。

使用重做日誌來保證持久性。

ACID 之間的關係

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

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

在這裏插入圖片描述

隔離級別

未提交讀(READ UNCOMMITTED)

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

提交讀(READ COMMITTED)

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

可重複讀(REPEATABLE READ)

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

可串行化(SERIALIZABLE)

強制事務串行執行。

需要加鎖實現,而其它隔離級別通常不需要。

隔離級別 髒讀 不可重複讀 幻影讀
未提交讀
提交讀 ×
可重複讀 × ×
可串行化 × × ×

InnoDB 可重複讀的實現

在 InnoDB 中,SELECT、UPDATE、DELETE 操作的不可重複讀問題可以通過 MVCC 來解決,但是 INSERT 操作的不可重複讀問題(幻讀問題)需要通過 MVCC + Next-Key Locks 來解決。

不可重複讀和幻讀

不可重複讀

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

在這裏插入圖片描述

幻讀

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

在這裏插入圖片描述

可以看出,幻讀問題是不可重複讀問題的子集。

不可重複讀和幻讀的區別

很多人容易搞混不可重複讀和幻讀,確實這兩者有些相似。但不可重複讀重點在於update和delete,而幻讀的重點在於insert。

如果使用鎖機制來實現這兩種隔離級別,在可重複讀中,該sql第一次讀取到數據後,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重複讀了。但這種方法卻無法鎖住insert的數據,所以當事務A先前讀取了數據,或者修改了全部數據,事務B還是可以insert數據提交,這時事務A就會發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這麼做可以有效的避免幻讀、不可重複讀、髒讀等問題,但會極大的降低數據庫的併發能力。

所以說不可重複讀和幻讀最大的區別,就在於如何通過鎖機制來解決他們產生的問題。

上文說的,是使用悲觀鎖機制,但是MySQL、ORACLE、PostgreSQL等成熟的數據庫,出於性能考慮,都是使用了以樂觀鎖爲理論基礎的MVCC(多版本併發控制)。

在 InnoDB 中,SELECT、UPDATE、DELETE 操作的不可重複讀問題可以通過 MVCC 來解決,但是 INSERT 操作的不可重複讀問題(幻讀問題)需要通過 MVCC + Next-Key Locks 來解決。

多版本併發控制(MVCC)

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

基礎概念

版本號

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

隱藏的列

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

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

Undo 日誌

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

在這裏插入圖片描述

實現過程

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

當開始一個事務時,該事務的版本號肯定大於當前所有數據行快照的創建版本號,理解這一點很關鍵。數據行快照的創建版本號是創建數據行快照時的系統版本號,系統版本號隨着創建事務而遞增,因此新創建一個事務時,這個事務的系統版本號比之前的系統版本號都大,也就是比所有數據行快照的創建版本號都大。

SELECT

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

把沒有對一個數據行做修改的事務稱爲 T,T 所要讀取的數據行快照的創建版本號必須小於等於 T 的版本號,因爲如果大於 T 的版本號,那麼表示該數據行快照是其它事務的最新修改,因此不能去讀取它。除此之外,T 所要讀取的數據行快照的刪除版本號必須是未定義或者大於 T 的版本號,因爲如果小於等於 T 的版本號,那麼表示該數據行快照是已經被刪除的,不應該去讀取它。

INSERT

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

DELETE

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

UPDATE

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

快照讀與當前讀

在可重複讀級別中,通過MVCC機制,雖然讓數據變得可重複讀,但我們讀到的數據可能是歷史數據,是不及時的數據,不是數據庫當前的數據!這在一些對於數據的時效特別敏感的業務中,就很可能出問題。

對於這種讀取歷史數據的方式,我們叫它快照讀 (snapshot read),而讀取數據庫當前版本數據的方式,叫當前讀 (current read)。很顯然,在MVCC中:

快照讀:就是select。

  • select * from table ….;

當前讀:特殊的讀操作,插入/更新/刪除操作,屬於當前讀,處理的都是當前的數據,需要加鎖。

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

事務的隔離級別實際上都是定義了當前讀的級別,MySQL爲了減少鎖處理(包括等待其它鎖)的時間,提升併發能力,引入了快照讀的概念,使得select不用加鎖。而update、insert這些“當前讀”,就需要另外的模塊來解決了。

Next-Key Locks

Next-Key Locks 是 MySQL 的 InnoDB 存儲引擎的一種鎖實現。

MVCC 不能解決幻影讀問題,Next-Key Locks 就是爲了解決這個問題而存在的。在可重複讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Locks 可以解決幻讀問題。

Record Locks

鎖定一個記錄上的索引,而不是記錄本身。

如果表沒有設置索引,InnoDB 會自動在主鍵上創建隱藏的聚簇索引,因此 Record Locks 依然可以使用。

Gap Locks

鎖定索引之間的間隙,但是不包含索引本身。例如當一個事務執行以下語句,其它事務就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

它是 Record Locks 和 Gap Locks 的結合,不僅鎖定一個記錄上的索引,也鎖定索引之間的間隙。例如一個索引包含以下值:10, 11, 13, and 20,那麼就需要鎖定以下區間:

(-, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +)

示例講解

MySQL是這麼實現的:

在class_teacher這張表中,teacher_id是個索引,那麼它就會維護一套B+樹的數據關係,爲了簡化,我們用鏈表結構來表達(實際上是個樹形結構,但原理相同)

如圖所示,InnoDB使用的是聚集索引,teacher_id身爲二級索引,就要維護一個索引字段和主鍵id的樹狀結構(這裏用鏈表形式表現),並保持順序排列。

Innodb將這段數據分成幾個個區間

  • (negative infinity, 5],
  • (5,30],
  • (30,positive infinity);

update class_teacher set class_name=‘初三四班’ where teacher_id=30;不僅用行鎖,鎖住了相應的數據行;同時也在兩邊的區間,(5,30]和(30,positive infinity),都加入了gap鎖。這樣事務B就無法在這個兩個區間insert進新數據。

受限於這種實現方式,Innodb很多時候會鎖住不需要鎖的區間。如下所示:

事務A 事務B 事務C
begin; begin; begin;
select id,class_name,teacher_id from class_teacher;
表字段:id class_name teacher_id
update class_teacher set class_name=‘初一一班’ where teacher_id=20;
insert into class_teacher values (null,‘初三五班’,10);waiting … insert into class_teacher values (null,‘初三五班’,40);
commit; 事務A commit之後,這條語句才插入成功 commit;
commit;

update的teacher_id=20是在(5,30]區間,即使沒有修改任何數據,Innodb也會在這個區間加gap鎖,而其它區間不會影響,事務C正常插入。

如果使用的是沒有索引的字段,比如update class_teacher set teacher_id=7 where class_name=‘初三八班(即使沒有匹配到任何數據)’,那麼會給全表加入gap鎖。同時,它不能像上文中行鎖一樣經過MySQL Server過濾自動解除不滿足條件的鎖,因爲沒有索引,則這些字段也就沒有排序,也就沒有區間。除非該事務提交,否則其它事務無法插入任何數據。

行鎖防止別的事務修改或刪除,GAP鎖防止別的事務新增,行鎖和GAP鎖結合形成的的Next-Key鎖共同解決了RR級別在寫數據時的幻讀問題。

歡迎關注我的公衆號:荒古傳說

本文作者: 荒古
本文鏈接: https://haxianhe.com/2019/08/12/MySQL學習筆記之InnoDB事務處理/
版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 許可協議。轉載請註明出處!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章