一文說盡MySQL事務及ACID特性的實現原理

本文將首先介紹 MySQL 事務相關的基礎概念,而後介紹事務的 ACID 特性,並分析其實現原理。MySQL 博大精深,文章疏漏之處在所不免,歡迎批評指正。sql

MySQL 事務基礎概念數據庫

事務(Transaction)是訪問和更新數據庫的程序執行單元;事務中可能包含一個或多個 sql 語句,這些語句要麼都執行,要麼都不執行。緩存

做爲一個關係型數據庫,MySQL 支持事務,本文介紹基於 MySQL 5.6。首先回顧一下 MySQL 事務的基礎知識。服務器

邏輯架構和存儲引擎架構

如上圖所示,MySQL 服務器邏輯架構從上往下能夠分爲三層:併發

  • 第一層:處理客戶端鏈接、受權認證等。
  • 第二層:服務器層,負責查詢語句的解析、優化、緩存以及內置函數的實現、存儲過程等。
  • 第三層:存儲引擎,負責 MySQL 中數據的存儲和提取。MySQL 中服務器層無論理事務,事務是由存儲引擎實現的。

MySQL 支持事務的存儲引擎有 InnoDB、NDB Cluster 等,其中 InnoDB 的使用最爲普遍;其餘存儲引擎不支持事務,如 MyIsam、Memory 等。函數

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

提交和回滾學習

典型的 MySQL 事務是以下操做的:大數據

 
  1. start transaction; 
  2. ……  #一條或多條sql語句 
  3. 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 命令,都不會強制提交事務。

ACID 特性

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

  • 原子性(Atomicity,或稱不可分割性)
  • 一致性(Consistency)
  • 隔離性(Isolation)
  • 持久性(Durability)

按照嚴格的標準,只有同時知足 ACID 特性纔是事務;可是在各大數據庫廠商的實現中,真正知足 ACID 的事務少之又少。

例如 MySQL 的 NDB Cluster 事務不知足持久性和隔離性;InnoDB 默認事務隔離級別是可重複讀,不知足隔離性;Oracle 默認的事務隔離級別爲 READ COMMITTED,不知足隔離性……

所以與其說 ACID 是事務必須知足的條件,不如說它們是衡量事務的四個維度。

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

ACID 特性及其實現原理

原子性

定義

原子性是指一個事務是一個不可分割的工做單位,其中的操做要麼都作,要麼都不作。

若是事務中一個 sql 語句執行失敗,則已執行的語句也必須回滾,數據庫退回到事務前的狀態。

實現原理: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 以前的狀態。

持久性

定義

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

實現原理: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 中修改的數據寫入磁盤(即刷髒)要快呢?

主要有如下兩方面的緣由:

  • 刷髒是隨機 IO,由於每次修改的數據位置隨機,但寫 redo log 是追加操做,屬於順序 IO。
  • 刷髒是以數據頁(Page)爲單位的,MySQL 默認頁大小是 16KB,一個 Page 上一個小修改都要整頁寫入;而 redo log 中只包含真正須要寫入的部分,無效 IO 大大減小。

redo log 與 binlog

咱們知道,在 MySQL 中還存在 binlog(二進制日誌)也能夠記錄寫操做並用於數據的恢復,但兩者是有着根本的不一樣的。

做用不一樣:

  • redo log 是用於 crash recovery 的,保證 MySQL 宕機也不會影響持久性;
  • binlog 是用於 point-in-time recovery 的,保證服務器能夠基於時間點恢復數據,此外 binlog 還用於主從複製。

層次不一樣:

  • redo log 是 InnoDB 存儲引擎實現的,
  • 而 binlog 是 MySQL 的服務器層(能夠參考文章前面對 MySQL 邏輯架構的介紹)實現的,同時支持 InnoDB 和其餘存儲引擎。

內容不一樣:

  • redo log 是物理日誌,內容基於磁盤的 Page。
  • binlog 是邏輯日誌,內容是一條條 sql。

寫入時機不一樣:

  • redo log 的寫入時機相對多元。前面曾提到,當事務提交時會調用 fsync 對 redo log 進行刷盤;這是默認狀況下的策略,修改 innodb_flush_log_at_trx_commit 參數能夠改變該策略,但事務的持久性將沒法保證。

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

  • binlog 在事務提交時寫入。

隔離性

定義

與原子性、持久性側重於研究事務自己不一樣,隔離性研究的是不一樣事務之間的相互影響。

隔離性是指事務內部的操做與其餘事務是隔離的,併發執行的各個事務之間不能互相干擾。

嚴格的隔離性,對應了事務隔離級別中的 Serializable(可串行化),但實際應用中出於性能方面的考慮不多會使用可串行化。

隔離性追求的是併發情形下事務之間互不干擾。簡單起見,咱們僅考慮最簡單的讀操做和寫操做(暫時不考慮帶鎖讀等特殊操做)。

那麼隔離性的探討,主要能夠分爲兩個方面:

  • (一個事務)寫操做對(另外一個事務)寫操做的影響:鎖機制保證隔離性。
  • (一個事務)寫操做對(另外一個事務)讀操做的影響:MVCC 保證隔離性。

鎖機制

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

鎖機制的基本原理能夠歸納爲:

  • 事務在修改數據以前,須要先得到相應的鎖。
  • 得到鎖以後,事務即可以修改數據。
  • 該事務操做期間,這部分數據是鎖定的,其餘事務若是須要修改數據,須要等待當前事務提交或回滾後釋放鎖。

行鎖與表鎖:按照粒度,鎖能夠分爲表鎖、行鎖以及其餘位於兩者之間的鎖。

表鎖在操做數據時會鎖定整張表,併發性能較差;行鎖則只鎖定須要操做的數據,併發性能好。

可是因爲加鎖自己須要消耗資源(得到鎖、檢查鎖、釋放鎖等都須要消耗資源),所以在鎖定數據較多狀況下使用表鎖能夠節省大量資源。

MySQL 中不一樣的存儲引擎支持的鎖是不同的,例如 MyIsam 只支持表鎖,而 InnoDB 同時支持表鎖和行鎖,且出於性能考慮,絕大多數狀況下使用的都是行鎖。

如何查看鎖信息?有多種方法能夠查看 InnoDB 中鎖的狀況,例如:

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

下面來看一個例子:

 
  1. #在事務A中執行: 
  2. start transaction; 
  3. update account SET balance = 1000 where id = 1; 
  4. 在事務B中執行: 
  5. start transaction; 
  6. update account SET balance = 2000 where id = 1; 

此時查看鎖的狀況:

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

經過上述命令能夠查看事務 24052 和 24053 佔用鎖的狀況;其中 lock_type 爲 RECORD,表明鎖爲行鎖(記錄鎖);lock_mode 爲 X,表明排它鎖(寫鎖)。

除了排它鎖(寫鎖)以外,MySQL 中還有共享鎖(讀鎖)的概念。因爲本文重點是 MySQL 事務的實現原理,所以對鎖的介紹到此爲止。

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

髒讀、不可重複讀和幻讀

首先來看併發狀況下,讀操做可能存在的三類問題。

①髒讀:當前事務(A)中能夠讀到其餘事務(B)未提交的數據(髒數據),這種現象是髒讀。

舉例以下(以帳戶餘額表爲例):

②不可重複讀:在事務 A 中前後兩次讀取同一個數據,兩次讀取的結果不同,這種現象稱爲不可重複讀。

髒讀與不可重複讀的區別在於:前者讀到的是其餘事務未提交的數據,後者讀到的是其餘事務已提交的數據。

舉例以下:

③幻讀:在事務 A 中按照某個條件前後兩次查詢數據庫,兩次查詢結果的條數不一樣,這種現象稱爲幻讀。

不可重複讀與幻讀的區別能夠通俗的理解爲:前者是數據變了,後者是數據的行數變了。

舉例以下:

事務隔離級別

sql 標準中定義了四種隔離級別,並規定了每種隔離級別下上述幾個問題是否存在。

通常來講,隔離級別越低,系統開銷越低,可支持的併發越高,但隔離性也越差。

隔離級別與讀問題的關係以下:

在實際應用中,讀未提交在併發時會致使不少問題,而性能相對於其餘隔離級別提升卻頗有限,所以使用較少。

可串行化強制事務串行,併發效率很低,只有當對數據一致性要求極高且能夠接受沒有併發時使用,所以使用也較少。

所以在大多數數據庫系統中,默認的隔離級別是讀已提交(如 Oracle)或可重複讀(後文簡稱 RR)。

能夠經過以下兩個命令分別查看全局隔離級別和本次會話的隔離級別:

InnoDB 默認的隔離級別是 RR,後文會重點介紹 RR。須要注意的是,在 SQL 標準中,RR 是沒法避免幻讀問題的,可是 InnoDB 實現的 RR 避免了幻讀問題。

MVCC

RR 解決髒讀、不可重複讀、幻讀等問題,使用的是 MVCC:MVCC 全稱 Multi-Version Concurrency Control,即多版本的併發控制協議。

下面的例子很好的體現了 MVCC 的特色:在同一時刻,不一樣的事務讀取到的數據多是不一樣的(即多版本)——在 T5 時刻,事務 A 和事務 C 能夠讀取到不一樣版本的數據。

MVCC 最大的優勢是讀不加鎖,所以讀寫不衝突,併發性能好。InnoDB 實現 MVCC,多個版本的數據能夠共存,主要是依靠數據的隱藏列(也能夠稱之爲標記位)和 undo log。

其中數據的隱藏列包括了該行數據的版本號、刪除時間、指向 undo log 的指針等等。

當讀取數據時,MySQL 能夠經過隱藏列判斷是否須要回滾並找到回滾須要的 undo log,從而實現 MVCC;隱藏列的詳細格式再也不展開。

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

①髒讀

當事務 A 在 T3 時間節點讀取 zhangsan 的餘額時,會發現數據已被其餘事務修改,且狀態爲未提交。

此時事務 A 讀取最新數據後,根據數據的 undo log 執行回滾操做,獲得事務 B 修改前的數據,從而避免了髒讀。

②不可重複讀

當事務 A 在 T2 節點第一次讀取數據時,會記錄該數據的版本號(數據的版本號是以 row 爲單位記錄的),假設版本號爲 1;當事務 B 提交時,該行記錄的版本號增長,假設版本號爲 2。

當事務 A 在 T5 再一次讀取數據時,發現數據的版本號(2)大於第一次讀取時記錄的版本號(1),所以會根據 undo log 執行回滾操做,獲得版本號爲 1 時的數據,從而實現了可重複讀。

③幻讀

InnoDB 實現的 RR 經過 next-keylock 機制避免了幻讀現象。

next-keylock 是行鎖的一種,實現至關於 record lock(記錄鎖) + gap lock(間隙鎖);其特色是不只會鎖住記錄自己(record lock 的功能),還會鎖定一個範圍(gap lock 的功能)。

固然,這裏咱們討論的是不加鎖讀:此時的 next-key lock 並非真的加鎖,只是爲讀取的數據增長了標記(標記內容包括數據的版本號等);準確起見姑且稱之爲類 next-key lock 機制。

仍是之前面的例子來講明:

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

這樣當 T5 時刻再次讀取 0

小結:歸納來講,InnoDB 實現的 RR,經過鎖機制、數據的隱藏列、undo log 和類 next-key lock,實現了必定程度的隔離性,能夠知足大多數場景的須要。

不過須要說明的是,RR 雖然避免了幻讀問題,可是畢竟不是 Serializable,不能保證徹底的隔離。

下面是一個例子,你們能夠本身驗證一下:

一致性

基本概念

一致性是指事務執行結束後,數據庫的完整性約束沒有被破壞,事務執行的先後都是合法的數據狀態。

數據庫的完整性約束包括但不限於:

  • 實體完整性(如行的主鍵存在且惟一)
  • 列完整性(如字段的類型、大小、長度要符合要求)
  • 外鍵約束
  • 用戶自定義完整性(如轉帳先後,兩個帳戶餘額的和應該不變)

實現

能夠說,一致性是事務追求的最終目標:前面提到的原子性、持久性和隔離性,都是爲了保證數據庫狀態的一致性。此外,除了數據庫層面的保障,一致性的實現也須要應用層面進行保障。

實現一致性的措施包括:

  • 保證原子性、持久性和隔離性,若是這些特性沒法保證,事務的一致性也沒法保證。
  • 數據庫自己提供保障,例如不容許向整形列插入字符串值、字符串長度不能超過列的限制等。
  • 應用層面進行保障,例如若是轉帳操做只扣除轉帳者的餘額,而沒有增長接收者的餘額,不管數據庫實現的多麼完美,也沒法保證狀態的一致。

總結

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

  • 原子性:語句要麼全執行,要麼全不執行,是事務最核心的特性。事務自己就是以原子性來定義的;實現主要基於 undo log。
  • 持久性:保證事務提交後不會由於宕機等緣由致使數據丟失;實現主要基於 redo log。
  • 隔離性:保證事務執行儘量不受其餘事務影響;InnoDB 默認的隔離級別是 RR,RR 的實現主要基於鎖機制、數據的隱藏列、undo log 和類 next-key lock 機制。
  • 一致性:事務追求的最終目標,一致性的實現既須要數據庫層面的保障,也須要應用層面的保障。

感興趣的能夠本身來個人Java架構羣,能夠獲取免費的學習資料,羣號:855801563對Java技術,架構技術感興趣的同窗,歡迎加羣,一塊兒學習,相互討論。

相關文章
相關標籤/搜索