歡迎光臨
每天分享高質量文章

深入學習MySQL事務:ACID特性的實現原理

 

事務是MySQL等關係型資料庫區別於NoSQL的重要方面,是保證資料一致性的重要手段。本文將首先介紹MySQL事務相關的基礎概念,然後介紹事務的ACID特性,並分析其實現原理。

 

MySQL博大精深,文章疏漏之處在所難免,歡迎批評指正。

一、基礎概念

事務(Transaction)是訪問和更新資料庫的程式執行單元;事務中可能包含一個或多個sql陳述句,這些陳述句要麼都執行,要麼都不執行。作為一個關係型資料庫,MySQL支持事務,本文介紹基於MySQL5.6。

 

首先回顧一下MySQL事務的基礎知識。

1、邏輯架構和儲存引擎

 

 

圖片來源:https://blog.csdn.net/fuzhongmin05/article/details/70904190

 

如上圖所示,MySQL服務器邏輯架構從上往下可以分為三層:

 

(1)第一層:處理客戶端連接、授權認證等。

(2)第二層:服務器層,負責查詢陳述句的解析、優化、快取以及內置函式的實現、儲存過程等。

(3)第三層:儲存引擎,負責MySQL中資料的儲存和提取。MySQL中服務器層不管理事務,事務是由儲存引擎實現的。MySQL支持事務的儲存引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最為廣泛;其他儲存引擎不支持事務,如MyIsam、Memory等。

 

如無特殊說明,後文中描述的內容都是基於InnoDB。

 

2、提交和回滾

 

典型的MySQL事務是如下操作的:

start transaction;
……  #一條或多條sql陳述句
commit;

其中start transaction標識事務開始,commit提交事務,將執行結果寫入到資料庫。如果sql陳述句執行出現問題,會呼叫rollback,回滾所有已經執行成功的sql陳述句。當然,也可以在事務中直接使用rollback陳述句進行回滾。

 

自動提交

 

MySQL中預設採用的是自動提交(autocommit)樣式,如下所示:

 

 

在自動提交樣式下,如果沒有start transaction顯式地開始一個事務,那麼每個sql陳述句都會被當做一個事務執行提交操作。

 

通過如下方式,可以關閉autocommit;需要註意的是,autocommit引數是針對連接的,在一個連接中修改了引數,不會對其他連接產生影響。

 

 

如果關閉了autocommit,則所有的sql陳述句都在一個事務中,直到執行了commit或rollback,該事務結束,同時開始了另外一個事務。

 

特殊操作

 

在MySQL中,存在一些特殊的命令,如果在事務中執行了這些命令,會馬上強制執行commit提交事務;如DDL陳述句(create table/drop table/alter/table)、lock tables陳述句等等。

 

不過,常用的select、insert、update和delete命令,都不會強制提交事務。

 

3、ACID特性

 

ACID是衡量事務的四個特性:

 

  • 原子性(Atomicity,或稱不可分割性)

  • 一致性(Consistency)

  • 隔離性(Isolation)

  • 持久性(Durability)

 

按照嚴格的標準,只有同時滿足ACID特性才是事務;但是在各大資料庫廠商的實現中,真正滿足ACID的事務少之又少。例如MySQL的NDB Cluster事務不滿足持久性和隔離性;InnoDB預設事務隔離級別是可重覆讀,不滿足隔離性;Oracle預設的事務隔離級別為READ COMMITTED,不滿足隔離性……因此與其說ACID是事務必須滿足的條件,不如說它們是衡量事務的四個維度。

 

下麵將詳細介紹ACID特性及其實現原理;為了便於理解,介紹的順序不是嚴格按照A-C-I-D。

二、原子性

1、定義

 

原子性是指一個事務是一個不可分割的工作單位,其中的操作要麼都做,要麼都不做;如果事務中一個sql陳述句執行失敗,則已執行的陳述句也必須回滾,資料庫退回到事務前的狀態。

 

2、實現原理:undo log

 

在說明原子性原理之前,首先介紹一下MySQL的事務日誌。MySQL的日誌有很多種,如二進制日誌、錯誤日誌、查詢日誌、慢查詢日誌等,此外InnoDB儲存引擎還提供了兩種事務日誌:redo log(重做日誌)和undo log(回滾日誌)。其中redo log用於保證事務持久性;undo log則是事務原子性和隔離性實現的基礎。

 

下麵說回undo log。實現原子性的關鍵,是當事務回滾時能夠撤銷所有已經成功執行的sql陳述句。InnoDB實現回滾,靠的是undo log:當事務對資料庫進行修改時,InnoDB會生成對應的undo log;如果事務執行失敗或呼叫了rollback,導致事務需要回滾,便可以利用undo log中的信息將資料回滾到修改之前的樣子。

 

undo log屬於邏輯日誌,它記錄的是sql執行相關的信息。當發生回滾時,InnoDB會根據undo log的內容做與之前相反的工作:對於每個insert,回滾時會執行delete;對於每個delete,回滾時會執行insert;對於每個update,回滾時會執行一個相反的update,把資料改回去。

 

以update操作為例:當事務執行update時,其生成的undo log中會包含被修改行的主鍵(以便知道修改了哪些行)、修改了哪些列、這些列在修改前後的值等信息,回滾時便可以使用這些信息將資料還原到update之前的狀態。

三、持久性

1、定義

 

持久性是指事務一旦提交,它對資料庫的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。

 

2、實現原理:redo log

 

redo log和undo log都屬於InnoDB的事務日誌。下麵先聊一下redo log存在的背景。

 

InnoDB作為MySQL的儲存引擎,資料是存放在磁盤中的,但如果每次讀寫資料都需要磁盤IO,效率會很低。為此,InnoDB提供了快取(Buffer Pool),Buffer Pool中包含了磁盤中部分資料頁的映射,作為訪問資料庫的緩衝:當從資料庫讀取資料時,會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取後放入Buffer Pool;當向資料庫寫入資料時,會首先寫入Buffer Pool,Buffer Pool中修改的資料會定期掃清到磁盤中(這一過程稱為刷臟)。

 

Buffer Pool的使用大大提高了讀寫資料的效率,但是也帶了新的問題:如果MySQL宕機,而此時Buffer Pool中修改的資料還沒有掃清到磁盤,就會導致資料的丟失,事務的持久性無法保證。

於是,redo log被引入來解決這個問題:當資料修改時,除了修改Buffer Pool中的資料,還會在redo log記錄這次操作;當事務提交時,會呼叫fsync接口對redo log進行刷盤。如果MySQL宕機,重啟時可以讀取redo log中的資料,對資料庫進行恢復。redo log採用的是WAL(Write-ahead logging,預寫式日誌),所有修改先寫入日誌,再更新到Buffer Pool,保證了資料不會因MySQL宕機而丟失,從而滿足了持久性要求。

 

既然redo log也需要在事務提交時將日誌寫入磁盤,為什麼它比直接將Buffer Pool中修改的資料寫入磁盤(即刷臟)要快呢?主要有以下兩方面的原因:

 

(1)刷臟是隨機IO,因為每次修改的資料位置隨機,但寫redo log是追加操作,屬於順序IO。

(2)刷臟是以資料頁(Page)為單位的,MySQL預設頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效IO大大減少。

3、redo log與binlog

 

們知道,在MySQL中還存在binlog(二進制日誌)也可以記錄寫操作並用於資料的恢復,但二者是有著根本的不同的:

 

(1)作用不同:redo log是用於crash recovery的,保證MySQL宕機也不會影響持久性;binlog是用於point-in-time recovery的,保證服務器可以基於時間點恢復資料,此外binlog還用於主從複製。

 

(2)層次不同:redo log是InnoDB儲存引擎實現的,而binlog是MySQL的服務器層(可以參考文章前面對MySQL邏輯架構的介紹)實現的,同時支持InnoDB和其他儲存引擎。

 

(3)內容不同:redo log是物理日誌,內容基於磁盤的Page;binlog是邏輯日誌,內容是一條條sql。

 

(4)寫入時機不同:binlog在事務提交時寫入;redo log的寫入時機相對多元:

 

  • 前面曾提到:當事務提交時會呼叫fsync對redo log進行刷盤;這是預設情況下的策略,修改innodb_flush_log_at_trx_commit引數可以改變該策略,但事務的持久性將無法保證。

  • 除了事務提交時,還有其他刷盤時機:如master thread每秒刷盤一次redo log等,這樣的好處是不一定要等到commit時刷盤,commit速度大大加快。

四、隔離性

1、定義

 

與原子性、持久性側重於研究事務本身不同,隔離性研究的是不同事務之間的相互影響。隔離性是指,事務內部的操作與其他事務是隔離的,併發執行的各個事務之間不能互相干擾。嚴格的隔離性,對應了事務隔離級別中的Serializable (可串行化),但實際應用中出於性能方面的考慮很少會使用可串行化。

 

隔離性追求的是併發情形下事務之間互不干擾。簡單起見,我們僅考慮最簡單的讀操作和寫操作(暫時不考慮帶鎖讀等特殊操作),那麼隔離性的探討,主要可以分為兩個方面:

 

  • (一個事務)寫操作對(另一個事務)寫操作的影響:鎖機制保證隔離性

  • (一個事務)寫操作對(另一個事務)讀操作的影響:MVCC保證隔離性

 

2、鎖機制

 

首先來看兩個事務的寫操作之間的相互影響。隔離性要求同一時刻只能有一個事務對資料進行寫操作,InnoDB通過鎖機制來保證這一點。

 

鎖機制的基本原理可以概括為:事務在修改資料之前,需要先獲得相應的鎖;獲得鎖之後,事務便可以修改資料;該事務操作期間,這部分資料是鎖定的,其他事務如果需要修改資料,需要等待當前事務提交或回滾後釋放鎖。

 

行鎖與表鎖

 

按照粒度,鎖可以分為表鎖、行鎖以及其他位於二者之間的鎖。表鎖在運算元據時會鎖定整張表,併發性能較差;行鎖則只鎖定需要操作的資料,併發性能好。但是由於加鎖本身需要消耗資源(獲得鎖、檢查鎖、釋放鎖等都需要消耗資源),因此在鎖定資料較多情況下使用表鎖可以節省大量資源。MySQL中不同的儲存引擎支持的鎖是不一樣的,例如MyIsam只支持表鎖,而InnoDB同時支持表鎖和行鎖,且出於性能考慮,絕大多數情況下使用的都是行鎖。

 

如何查看鎖信息

 

有多種方法可以查看InnoDB中鎖的情況,例如:

select * from information_schema.innodb_locks; #鎖的概況
show engine innodb status#InnoDB整體狀態,其中包括鎖的情況

下麵來看一個例子:

 

#在事務A中執行:
start transaction;
update account SET balance = 1000 where id = 1;
#在事務B中執行:
start transaction;
update account SET balance = 2000 where id = 1;

此時查看鎖的情況:

 

show engine innodb status查看鎖相關的部分:

 

通過上述命令可以查看事務24052和24053占用鎖的情況;其中lock_type為RECORD,代表鎖為行鎖(記錄鎖);lock_mode為X,代表排它鎖(寫鎖)。

 

除了排它鎖(寫鎖)之外,MySQL中還有共享鎖(讀鎖)的概念。由於本文重點是MySQL事務的實現原理,因此對鎖的介紹到此為止,後續會專門寫文章分析MySQL中不同鎖的區別、使用場景等,歡迎關註。

 

介紹完寫操作之間的相互影響,下麵討論寫操作對讀操作的影響。

 

3、臟讀、不可重覆讀和幻讀

 

首先來看併發情況下,讀操作可能存在的三類問題:

 

(1)臟讀:當前事務(A)中可以讀到其他事務(B)未提交的資料(臟資料),這種現象是臟讀。舉例如下(以賬戶餘額表為例):

 

 

(2)不可重覆讀:在事務A中先後兩次讀取同一個資料,兩次讀取的結果不一樣,這種現象稱為不可重覆讀。臟讀與不可重覆讀的區別在於:前者讀到的是其他事務未提交的資料,後者讀到的是其他事務已提交的資料。舉例如下:

 

 

(3)幻讀:在事務A中按照某個條件先後兩次查詢資料庫,兩次查詢結果的條數不同,這種現象稱為幻讀。不可重覆讀與幻讀的區別可以通俗的理解為:前者是資料變了,後者是資料的行數變了。舉例如下:

 

4、事務隔離級別

 

SQL標準中定義了四種隔離級別,並規定了每種隔離級別下上述幾個問題是否存在。一般來說,隔離級別越低,系統開銷越低,可支持的併發越高,但隔離性也越差。隔離級別與讀問題的關係如下:

 

 

在實際應用中,讀未提交在併發時會導致很多問題,而性能相對於其他隔離級別提高卻很有限,因此使用較少。可串行化強制事務串行,併發效率很低,只有當對資料一致性要求極高且可以接受沒有併發時使用,因此使用也較少。因此在大多數資料庫系統中,預設的隔離級別是讀已提交(如Oracle)或可重覆讀(後文簡稱RR)。

 

以通過如下兩個命令分別查看全域性隔離級別和本次會話的隔離級別:

 

InnoDB預設的隔離級別是RR,後文會重點介紹RR。需要註意的是,在SQL標準中,RR是無法避免幻讀問題的,但是InnoDB實現的RR避免了幻讀問題。

 

5、MVCC

 

RR解決臟讀、不可重覆讀、幻讀等問題,使用的是MVCC:MVCC全稱Multi-Version Concurrency Control,即多版本的併發控制協議。下麵的例子很好的體現了MVCC的特點:在同一時刻,不同的事務讀取到的資料可能是不同的(即多版本)——在T5時刻,事務A和事務C可以讀取到不同版本的資料。

 

 

MVCC最大的優點是讀不加鎖,因此讀寫不衝突,併發性能好。InnoDB實現MVCC,多個版本的資料可以共存,主要是依靠資料的隱藏列(也可以稱之為標記位)和undo log。其中資料的隱藏列包括了該行資料的版本號、刪除時間、指向undo log的指標等等;當讀取資料時,MySQL可以通過隱藏列判斷是否需要回滾並找到回滾需要的undo log,從而實現MVCC;隱藏列的詳細格式不再展開。

 

下麵結合前文提到的幾個問題分別說明。

 

(1)臟讀

 

當事務A在T3時間節點讀取zhangsan的餘額時,會發現資料已被其他事務修改,且狀態為未提交。此時事務A讀取最新資料後,根據資料的undo log執行回滾操作,得到事務B修改前的資料,從而避免了臟讀。

 

(2)不可重覆讀

 

當事務A在T2節點第一次讀取資料時,會記錄該資料的版本號(資料的版本號是以row為單位記錄的),假設版本號為1;當事務B提交時,該行記錄的版本號增加,假設版本號為2;當事務A在T5再一次讀取資料時,發現資料的版本號(2)大於第一次讀取時記錄的版本號(1),因此會根據undo log執行回滾操作,得到版本號為1時的資料,從而實現了可重覆讀。

 

(3)幻讀

 

InnoDB實現的RR通過next-key lock機制避免了幻讀現象。

 

next-key lock是行鎖的一種,實現相當於record lock(記錄鎖) + gap lock(間隙鎖);其特點是不僅會鎖住記錄本身(record lock的功能),還會鎖定一個範圍(gap lock的功能)。當然,這裡我們討論的是不加鎖讀:此時的next-key lock並不是真的加鎖,只是為讀取的資料增加了標記(標記內容包括資料的版本號等);準確起見姑且稱之為類next-key lock機制。還是以前面的例子來說明:

 

 

當事務A在T2節點第一次讀取0

 

6、總結

 

概括來說,InnoDB實現的RR,通過鎖機制、資料的隱藏列、undo log和類next-key lock,實現了一定程度的隔離性,可以滿足大多數場景的需要。不過需要說明的是,RR雖然避免了幻讀問題,但是畢竟不是Serializable,不能保證完全的隔離,下麵是一個例子,大家可以自己驗證一下。

 

五、一致性

1、基本概念

 

一致性是指事務執行結束後,資料庫的完整性約束沒有被破壞,事務執行的前後都是合法的資料狀態。資料庫的完整性約束包括但不限於:物體完整性(如行的主鍵存在且唯一)、列完整性(如欄位的型別、大小、長度要符合要求)、外鍵約束、用戶自定義完整性(如轉賬前後,兩個賬戶餘額的和應該不變)。

 

2、實現

 

可以說,一致性是事務追求的最終標的:前面提到的原子性、持久性和隔離性,都是為了保證資料庫狀態的一致性。此外,除了資料庫層面的保障,一致性的實現也需要應用層面進行保障。

 

實現一致性的措施包括:

 

  • 保證原子性、持久性和隔離性,如果這些特性無法保證,事務的一致性也無法保證

  • 資料庫本身提供保障,例如不允許向整形列插入字串值、字串長度不能超過列的限制等

  • 應用層面進行保障,例如如果轉賬操作只扣除轉賬者的餘額,而沒有增加接收者的餘額,無論資料庫實現的多麼完美,也無法保證狀態的一致

六、總結

下麵總結一下ACID特性及其實現原理:

 

  • 原子性:陳述句要麼全執行,要麼全不執行,是事務最核心的特性,事務本身就是以原子性來定義的;實現主要基於undo log

  • 持久性:保證事務提交後不會因為宕機等原因導致資料丟失;實現主要基於redo log

  • 隔離性:保證事務執行盡可能不受其他事務影響;InnoDB預設的隔離級別是RR,RR的實現主要基於鎖機制、資料的隱藏列、undo log和類next-key lock機制

  • 一致性:事務追求的最終標的,一致性的實現既需要資料庫層面的保障,也需要應用層面的保障

參考文獻

《MySQL技術內幕:InnoDB儲存引擎》

《高性能MySQL》

《MySQL運維內參》

https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_acid

https://dev.mysql.com/doc/refman/5.6/en/innodb-next-key-locking.html

http://blog.sina.com.cn/s/blog_499740cb0100ugs7.html

https://mp.weixin.qq.com/s/2dwGBTmu_da2x-HiHlN0vw

http://www.cnblogs.com/chenpingzhao/p/5065316.html

https://juejin.im/entry/5ba0a254e51d450e735e4a1f

http://hedengcheng.com/?p=771

赞(0)

分享創造快樂