MySQL的事務機制和鎖(InnoDB引擎、MVCC多版本併發控制技術)

Life_Goes_On 發表於 2020-09-15

一、事務(資料庫的事務都通用的定義)

1.1 事務定義

事務是由一步或幾步資料庫操作序列組成邏輯執行單元,這系列操作要麼全部執行,要麼全部放棄執行。事務通常以 BEGIN TRANSACTION 開始,以COMMITROLLBACK 操作結束:

  • COMMIT 即提交,提交事務中所有的操作、事務正常結束;
  • ROLLBACK 即回滾,撤銷已做的所有操作,回滾到事務開始的狀態。

1.2 事務的四種特性

ACID: 原子性,一致性,隔離性,永續性。

ACID屬性 含義
原子性(Atomicity) 指事物在邏輯上是不可分割的操作單元,所有語句要麼都執行,要麼都撤銷執行。
一致性(Consistent) 一個事務本質是將資料從一種一致性狀態轉換到另一種一致性狀態,具體取決於現實生活的邏輯。(比如轉賬,A轉給B,操作前後A+B的錢是不變的)
隔離性(Isolation) 隔離性是針對併發事務而言的,同時處理多個事務的時候,資料庫的事務提供了不同的隔離級別來保證正確。
永續性(Durable) 事務一旦提交,對於資料的修改是永續性的,資料更新的結果已經從記憶體轉存到外部儲存器,即使系統故障,已提交的資料更新也不會丟失。

這四個特性在沒有併發的時候顯然很容易滿足,但是在併發處理事務的情況下,可能會帶來一些問題

問題 含義
丟失更新(Lost Update) 當兩個或多個事務操作同一行,後面的事務修改的值會覆蓋前面的事務修改的值。
髒讀(Dirty Reads) 一個事務讀到了被另一個事務修改,但尚未提交的事務。當一個事務正在多次修改一個資料,而這一系列修改還沒有最後提交,另一個併發事務來讀取了,就會導致錯誤。也就是另一個事務讀到了髒資料。
不可重複讀(Non-Repeatable Reads) 一個事務操作的過程裡,先讀取一個資料,後來又讀取,而兩次讀出的資料值不一致。就是因為中間被別的事務修改了。
幻讀(Phantom Reads) 一個事務按照相同的查詢條件查兩次,第一次查出了A集合,第二次卻不是了,因為其他事務插入了資料,正好滿足這個事務的查詢條件。

注意事項:

  • 髒讀不可重複讀的區別:髒讀讀到的髒資料是另一個事務沒有提交的資料,但是不可重複讀讀到錯誤資料是因為另一個事務把資料修改並提交了;
  • 幻讀不可重複讀的區別:幻讀和不可重複讀都是讀到了另一個事務提交的資料,但是不可重複讀是兩個事務針對同一個資料項,而幻讀針對的是一個資料整體(資料條目)

為了解決上述提到的事務併發問題,資料庫提供一定的事務隔離機制來解決這個問題。

資料庫的事務隔離越嚴格,併發副作用越小,但付出的代價也就越大,因為事務隔離實質上就是使用事務在一定程度上“序列化” 進行,這顯然與“併發” 是矛盾的。

隔離級別 中文事務 解決問題
RU:READ UNCOMMITED 未提交讀(很少使用,基本沒有解決問題) 丟失更新
RC:READ COMMITED 提交讀。顧名思義,保證一個事務只能看見另一個事務已經提交的事務的結果。 丟失更新+髒讀
RR:REPEATEABLE READ(Innodb預設) 可重複讀。顧名思義,解決了第三個併發問題:不可重複讀。 丟失更新+髒讀+不可重複讀
S:SERIALIZABLE 序列化。通過強制事務排序來讓他們序列執行。(也很少使用)本質上是給每個資料行都加上了共享鎖 四個問題都解決了

從上往下,隔離級別越來越高,但是代價肯定越來越大,真正選擇的時候需要斟酌,可以看到,要想真正解決幻讀問題,需要隔離級別為 S。

注意:

  • 事實上 Mysql 的 InnoDB 通過 MVCC (Multi-Version Concurrent Control,多版本併發控制)機制解決了不可重複讀的基礎上,又解決了幻讀的的問題。

二、MySQL的鎖(結合 InnoDB引擎)

2.1 背景

對於資料庫事務的併發控制技術有很多,基於鎖、基於時間戳、基於MVCC的併發控制、基於MVCC的可序列化快照隔離等。

而我們討論的概念,是MySQL的事務,再具體一些,是 InnoDB 支援的事務。

InnoDB 是支援 ACID 的,而MySQL用 InnoDB 作為自己的預設儲存引擎,事務管理是 MySQL Server 實現框架和介面定義,而 InnoDB 提供具體的事務操作和併發控制,所以 MySQL 的事務模型,主要是指 MySQL 的InnoDB 的事務管理部分。

InnoDB 使用鎖和 MVCC 技術來實現併發事務的訪問控制技術。
其中,鎖是併發控制的基礎,在此基礎之上,實現了 MVCC 機制,用以提高基於鎖的方式帶來的低效率問題。

有了這個概念,我們下面分別討論 MVCC 兩個內容。

  • MyISAM 預設使用的是表鎖;
  • InnoDB 支援行級鎖,加上 MySQL Server 支援表級鎖,所以結合事務的時候,使用 InnoDB 引擎,系統就預設使用的是行級鎖。

裡我們討論的鎖的實現,是為了事務的併發控制,所以都是在使用 InnoDB 引擎下的情況,那麼有一些概念可能在其他引擎下的實現也是類似的。

2.2 鎖的分類

從對資料操作的粒度分 :

  • 表鎖:操作時,會鎖定整個表。(MySQL Server
  • 行鎖:操作時,會鎖定當前操作行。(也叫 Record Lock記錄鎖),實際上他是在索引上的記錄之鎖,因為 InnoDB 的表的組織結構是通過 B+ 樹索引。

從對資料操作的型別分:

  • 讀鎖(共享鎖、S鎖):針對同一份資料,多個讀操作可以同時進行而不會互相影響。
  • 寫鎖(排它鎖、X鎖):當前操作沒有完成之前,它會阻斷其他寫鎖和讀鎖。
  • 意向共享鎖(IS):事務想要獲取一張表中某幾行的共享鎖
  • 意向排它鎖(IX):事務想要獲取一張表中的某幾行的排它鎖

前兩個很容易理解,他們的相容情況是:只有 S 鎖和 S 鎖是相容的,其他的組合都是互斥的。

因為鎖的粒度不同,這就允許事務表級和行級的鎖可以同時存在,所以 InnoDB 支援了額外的一種鎖叫,意向鎖(Intention Lock):

問題:innodb的意向鎖有什麼作用?
mysql官網上對於意向鎖的解釋中有這麼一句話
“The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.”
意思是說加意向鎖的目的是為了表明某個事務正在鎖定一行或者將要鎖定一行。
那麼,意向鎖的作用就是“表明”加鎖的意圖,可是為什麼要表明這個意圖呢?

如果僅僅鎖定一行僅僅需要加一個鎖,那麼就直接加鎖就好了,這裡要表明加鎖意圖的原因是因為要鎖定一行不僅僅是要加一個鎖,而是要做一系列操作嗎?

①在mysql中有表鎖,LOCK TABLE my_tabl_name READ; 用讀鎖鎖表,會阻塞其他事務修改表資料。LOCK
TABLE my_table_name WRITe; 用寫鎖鎖表,會阻塞其他事務讀和寫。
②Innodb引擎又支援行鎖,行鎖分為共享鎖,一個事務對一行的共享只讀鎖。排它鎖,一個事務對一行的排他讀寫鎖。
③這兩中型別的鎖共存的問題考慮這個例子:
事務A鎖住了表中的一行,讓這一行只能讀,不能寫。之後,事務B申請整個表的寫鎖。如果事務B申請成功,那麼理論上它就能修改表中的任意一行,這與A持有的行鎖是衝突的。
資料庫需要避免這種衝突,就是說要讓B的申請被阻塞,直到A釋放了行鎖。

資料庫要怎麼判斷這個衝突呢?
step1:判斷表是否已被其他事務用表鎖鎖表
step2:判斷表中的每一行是否已被行鎖鎖住。
注意step2,這樣的判斷方法效率實在不高,因為需要遍歷整個表。
於是就有了意向鎖。在意向鎖存在的情況下,事務A必須先申請表的意向共享鎖,成功後再申請一行的行鎖。在意向鎖存在的情況下, 上面的判斷可以改成
step1:不變
step2:發現表上有意向共享鎖,說明表中有些行被共享行鎖鎖住了,因此,事務B申請表的寫鎖會被阻塞。

注意:申請意向鎖的動作是資料庫完成的,就是說,事務A申請一行的行鎖的時候,資料庫會自動先開始申請表的意向鎖,不需要我們程式設計師使用程式碼來申請。

總結:為了實現多粒度鎖機制(白話:為了表鎖和行鎖都能用)

那麼這四個鎖在一起之後我們看看他們的相容性:

MySQL的事務機制和鎖(InnoDB引擎、MVCC多版本併發控制技術)

2.3 其他鎖

InnoDB裡還有幾個鎖:

  • 間隙鎖(Gap Locks:兩個索引項之間的間隔、稱為間隙,把這個間隙看作一個物件,在此物件上加鎖,就是間隙鎖。這個鎖是從加鎖的物件角度定義的鎖,所以和表、行是同一個角度的鎖。
  • Next-Key鎖(Next-Key Locks:行級鎖+間隙鎖共同組成。
  • Insert Intention Locks:基於間隙鎖,專門用於 Insert 操作。

間隙鎖會在 RC 隔離級別的某些情況下使用,在 RR 隔離級別下,間隙鎖會和行級鎖合併成 Next-key 鎖使用。(記住這一點)

2.4 鎖的施加細則

對於各種 MySQL 語句來說,InnoDB 對他們提供了事務操作的支援,這樣的支援就是通過併發控制的鎖來完成的。(當然,還有補充的 MVCC 技術,後面再說)

細則如下,(參考《資料庫事務處理的藝術》這本書裡對官方文件的總結,只選擇了常用的命令):

* SELECT...FOR UPDATE 或 SELECT...LOCK IN SHARE MODE:首先對掃描過的行加鎖(實際上是對索引的記錄上加鎖),如果掃描過的行不滿足 WHERE 條件則釋放鎖(但是有時候,鎖的釋放不及時比如 UNION 操作下被掃描過的行可能會被放到臨時表裡,那就直到查詢結束才會釋放鎖);
* ALTER TABLE ... LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}:在指定的表上施加讀鎖或者排他鎖;
* CREATE TABLE...SELECT...:其中的 SELECT 操作符合 SELECT 語句的枷鎖規則,只是不能帶有 FOR UPDATE 子句;
* DELETE FROM... WHERE...:在索引項上加排他的 Next-key 鎖;
* INSERT:在被插入的索引上加記錄鎖(意向鎖);
* INSERT... ON DUPLICATE KEY UPDATE:在被插入的索引項上加排他 Next-key 鎖;
* INSERT INTO T SELECT ... FROM S WHERE ...:對於被插入到 T 中的元組,在對應的索引項上施加排他記錄鎖。如果隔離級別是 RC ,則在表 S 對應的索引項上不加鎖,這是一個一致性讀操作;否則加上共享 Next-Key 鎖;
* SELECT ... LOCK IN SHARE MODE:在索引上施加共享 Next-Key 鎖;
* SELECT ... FROM ... FOR UPDATE:在索引項上施加排他 Next-Key 鎖,這樣的鎖會阻塞上一種”SELECT ... LOCK IN SHARE MODE“操作,但不會阻塞下一種 ”SELECT ... FROM“ 這樣的一致性讀操作;
* SELECT ... FROM 通常作為一個一致性讀操作,不需要加任何鎖。但是如果隔離級別是 S,那麼也要在索引項上加對應的共享 Next-Key 鎖;
* UPDATE ... WHERE ...:在索引項上施加排他的 Next-Key 鎖。
* 如果一個表上定義了外來鍵約束,那麼在出發約束條件被檢查的元組對應的索引項上,任何操作都會施加共享Next-Key 鎖。

LOCK TABLES 命令是在表級鎖,這個實現是 MySQL Server層的實現,InnoDB 則不會操作,那麼如果 InnoDB 不知道 MySQL Server設定了表級鎖,就還可能出現一個死鎖問題,實踐中需要注意。

三、InnoDB 的MVCC原理

在基於鎖的併發控制的基礎之上,實現了 MVCC 技術。

首先還是強調一點,MVCC 技術本身思想從名字就可以看的出來,就是通過多個版本進行併發的控制。那麼併發控制技術,是需要配合其他的併發控制技術來具體實現,這裡我們講的 InnoDB 的 MVCC 原理,就是基於鎖的。

3.1 日誌

日誌是保證事務的原子性、永續性的重要技術之一,在 MVCC 的實現中也是要用到的,這裡簡單介紹一下,對於每一個 SQL 操作,都不是一下子執行完成,因此資料的狀態都要變化,那麼把這個過程記錄下來,出現問題進行”回放“,就能應對事物的原子性和永續性。

需要記錄的資料通常包括:

  • 事務標識:比如事務 id;
  • 資料項的標識
  • 舊值:資料項被修改之前的值,又稱為 前像;
  • 新值:資料項被修改之後的值,又稱為後像。

一系列的 SQL 操作過程變成一個序列,這就是日誌,資料庫引擎在具體實現的時候會把日誌放到日誌快取區,然後刷出到外存,存放到日誌檔案。

日誌檔案一般分為 REDO 日誌和 UNDO 日誌:

  • REDO 日誌記錄事務的標識、資料項的標識和 新值;
  • UNDO 日誌記錄事務的標識、資料項的標識和 舊值。(InnoDB 的 MVCC 用到的是 UNDO log)

3.2 InnoDB 的MVCC

因為 InnoDB 的多版本,指的是 行(元組) 級別的版本,在每行(或者每個元組、每條記錄)上,都有一些和併發、回滾相關的隱含欄位,分別為:

  • DB_TRX_ID:很好理解,就是 id,表示上一個執行(insert | update)操作的事務。至於delete操作,InnoDB 認為是一個 update 操作,不過會更新一個另外的刪除位,將行表示為deleted。並非真正刪除。
  • DB_ROLL_PTR:就是 pointer,回滾指標,指向的就是一箇舊版本。那麼其實指向的是當前記錄行的 undo log 資訊,是舊版本的資料位於回滾段中的位置,通過這個指標能夠找到舊版本;
  • DB_ROW_ID:隨著新行插入而單調遞增的行 ID,和 MVCC 關係不大。

在回滾段裡的 UNDO 日誌分為兩種:

  • INSERT UNDO logs:插入到回滾段中的日誌,僅用於事務提交時使用,當事務提交,則插入 UNDO 日誌裡的內容被清除;
  • UPDATE UNDO logs:被用於一致性無鎖讀,為一致性讀提供快照隔離下的可被讀取的老版本資料。當沒有需要滿足一致性讀的快照時,一些老版本資料才能被清理。

以上,實現的原理基本告一段落,但是 InnoDB 的實現層面,還有另一個資料結構,就是 Read View 快照。

  • Read View(讀檢視),跟快照、snapshot 是一個概念,可以看作事務的生命週期裡面的一段,而不同的快照就是不同的段。在原始碼層面,他是一個類,名叫 ReadView,這裡面的內容,重點有兩個:
    • 一個就是儲存了快照的左右邊界
    • 另一個是提供瞭如何判斷當前行(元組)的可見性的標誌。

3.3 和 MVCC 有關的額外兩個概念

  • 快照讀(snapshot read):普通的 select 語句(不包括 select ... lock in share mode, select ... for update)。也就是不加鎖的非阻塞讀,所以在序列級別下的快照讀會退化成當前讀。他是基於多版本的,那麼快照讀可能讀到的並不一定是資料的最新版本,而有可能是之前的歷史版本。
  • 當前讀(current read) :select ... lock in share mode,select ... for update,insert,update,delete 語句。(為什麼叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。)

對於 InnoDB 的 MVCC
實現,很多部落格和書都是架空寫的原理,看了《高效能MySQL》,裡面也寫的是基於每個事務操作的時候給該行新增兩個版本號(當前版本號、刪除版本號),然後事務操作的時候根據版本號來判定是否執行完畢還是回滾等規則。現在看來並不是這樣,但是從上面提到的、原始碼裡實際增加的欄位來看,思想是大概類似的,不過實現的機制更加複雜。

四、MVCC 原理總結

4.1 總結 MVCC 原理

到這一步,概念有點多,我們來梳理一下。

  1. 首先,事務的概念有了,事務的特性隨著概念出來:ACID
  2. 那麼,併發事務如果不加控制,就會存在問題,按處理的難易程度從低到高:丟失更新-髒讀-不可重複讀-幻讀
  3. 於是,資料庫如果實現事務,就要保證特性,解決對應的問題,應運而生四個隔離級別RU-RC-RR-S
  4. 因為事務是在儲存引擎層實現的,所以接下來討論了基於 InnoDB 引擎的 MySQL 事務的實現:
    1. 相關概念:鎖的分類:表鎖、行鎖。讀鎖、寫鎖、意向鎖。間隙鎖(Gap Locks)、Next-Key鎖
    2. 相關概念:鎖的實施細則;
    3. 相關概念:基於鎖的併發版本控制,結合 MVCC。如果沒有 MVCC ,四個併發問題,除了讀讀不用加鎖,讀寫、寫讀、寫寫都不能併發執行,否則就會產生問題,效率低下,有了MVCC,讀寫和寫讀可以併發執行。那 MVCC 都用到了什麼呢?
      • 額外的幾個欄位
      • 基於 Undo log
      • 結合 Read View (快照)。

現在我們回過頭看一下MVCC機制工作的兩個隔離級別:RC 和 RR :

  • RC提交讀,要解決髒讀的問題,保證一個事務讀取到的一定是另一個事務的已經提交的結果,而不能是未提交的結果。那麼,對於 RC級別來說,務中,每次快照讀都會新生成一個快照和Read View,保證每個事務可以看到別的事務提交的更新。(關於具體的實現還有相應的演算法,可見性之類的);
  • RR可重複讀,要解決不可重複讀的問題,保證快照讀生成Read View時,Read View會記錄此時所有其他活動事務的快照,這些事務的修改對於當前事務都是不可見的。

4.2 MVCC 解決幻讀問題了嗎?

RR 級別下,沒有完全解決幻讀 的問題。

我們回憶幻讀的概念:一個事務按照相同的查詢條件查兩次,第一次查出了A集合,第二次卻不是了,因為其他事務插入了資料,正好滿足這個事務的查詢條件。

那麼對於上面的 MVCC 原理,基於快照讀的情況:

  1. 事務 A 開始後,執行普通 select 語句,建立了快照;
  2. 事務 B 執行 insert 語句;
  3. 事務 A 再執行普通 select 語句,得到的還是之前 B 沒有 insert 過的資料,因為這時候 A 讀的資料是符合快照可見性條件的資料。
  4. 這是解決了幻讀問題的。

但是考慮這種情況:

  1. 事務A執行的不是普通 select 語句,而是 select ... for update 等語句,根據上面的定義,事務 A 是當前讀,每次語句執行的時候都是獲取的最新資料。
  2. 那麼 B 執行 insert語句;
  3. A 再次查詢的時候,就可能會查到多一條資料,產生幻讀。

這個時候就要出場我們在前面的鎖分類部分的一行紅字,另外兩個鎖:間隙鎖和 Next-key Locks。

間隙鎖在 RR 級別發揮作用,結合行級鎖稱為 Next-key locks,解決幻讀問題。具體的演算法可能很複雜,原理就是鎖定範圍的設定加上了間隙,這樣插入操作肯定是沒辦法進行的,因此就不會存在其他事務的插入操作導致幻讀了。

到這裡我們可以得出結論,MySQL 裡完全解決幻讀的方法有兩個:

  1. 直接使用 S 隔離等級完全序列化;
  2. RR 的隔離級別結合 MVCC 機制,還要結合 間隙鎖。

五、其他

MySQL的事務機制和鎖(InnoDB引擎、MVCC多版本併發控制技術)
  • 關於資料庫的鎖,伺服器層面的實現預設是有表級別的鎖,沒有行鎖(書上沒有提這個點,但是網上也有說法講伺服器層面也實現了行鎖);
  • 不同的儲存引擎層面又以自己的方式實現了不同粒度級別的鎖,因此選擇引擎不同的時候,我們使用的鎖,瞭解的原理都是基於引擎的,另一方面,鎖總和事務聯絡在一起討論,不同的儲存引擎是否支援事務又是不一樣的,所以應該是把server層關於這一塊忽略掉了。

參考: