架構必知:MySQL 如何實現 ACID ?

寫在前面

本文主要探討 MySQL InnoDB 引擎下 ACID 的實現原理,對於諸如什麼是事務,隔離級別的含義等基礎知識不做過多闡述。

ACID

MySQL 作爲一個關係型數據庫,以最常見的 InnoDB 引擎來說,是如何保證 ACID 的。

隔離性

先說說隔離性,首先是四種隔離級別。

B0j4wq

不同的隔離級別是爲了解決不同的問題。也就是髒讀、幻讀、不可重複讀。

VaVeN0

那麼不同的隔離級別,隔離性是如何實現的,爲什麼不同事物間能夠互不干擾?答案是 鎖 和 MVCC。

先來說說鎖, MySQL 有多少鎖。

粒度

從粒度上來說就是表鎖、頁鎖、行鎖。
表鎖有意向共享鎖、意向排他鎖、自增鎖等。
行鎖是在引擎層由各個引擎自己實現的。但並不是所有的引擎都支持行鎖,比如 MyISAM 引擎就不支持行鎖。

行鎖的種類

在 InnoDB 事務中,行鎖通過給索引上的索引項加鎖來實現。這意味着只有通過索引條件檢索數據,InnoDB 才使用行級鎖,否則將使用表鎖。
行級鎖定同樣分爲兩種類型:共享鎖和排他鎖,以及加鎖前需要先獲得的意向共享鎖和意向排他鎖。

行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。

行鎖的實現算法

Record Lock

單個行記錄上的鎖,總是會去鎖住索引記錄。

Gap Lock

間隙鎖,想一下幻讀的原因,其實就是行鎖只能鎖住行,但新插入記錄這個動作,要更新的是記錄之間的 “間隙”。所以加入間隙鎖來解決幻讀。

Next-Key Lock

Gap Lock + Record Lock, 左開又閉。

鎖之於隔離性

大致介紹了下鎖,可以看到。有了鎖,當某事務正在寫數據時,其他事務獲取不到寫鎖,就無法寫數據,一定程度上保證了事務間的隔離。但前面說,加了寫鎖,爲什麼其他事務也能讀數據呢,不是獲取不到讀鎖嗎?

MVCC

前面說到,有了鎖,當前事務沒有寫鎖就不能修改數據,但還是能讀的,而且讀的時候,即使該行數據其他事務已修改且提交,還是可以重複讀到同樣的值。這就是 MVCC,多版本的併發控制,Multi-Version Concurrency Control。

版本鏈

Innodb 中行記錄的存儲格式,有一些額外的字段:DATA_TRX_ID 和 DATA_ROLL_PTR。

undo log : 記錄數據被修改之前的日誌,後面會詳細說。

ReadView

在每一條 SQL 開始的時候被創建,有幾個重要屬性:

開始查詢

現在開始查詢,一個 select 過來了,找到了一行數據。

RR 級別的幻讀

有了鎖和 MVCC , 事務的隔離性得到解決。這裏要引申一下,默認的 RR 的級別,解決了幻讀嗎?
幻讀通常針對的是 INSERT, 不可重複度則針對 UPDATE 。

6CnfXu

我們期望是

id  name



1   A



2   B

實際卻是

id  name



1   B



2   B

其實在 MySQL 可重複讀的隔離級別中並不是完全解決了幻讀的問題,而是解決了讀數據情況下的幻讀問題。而對於修改的操作依舊存在幻讀問題,就是說 MVCC 對於幻讀的解決時不徹底的。

原子性

接着說說原子性。前文有提到 undo log ,回滾日誌。隔離性的 MVCC 其實就是依靠它來實現的,原子性也是。
實現原子性的關鍵,是當事務回滾時能夠撤銷所有已經成功執行的 sql 語句。
當事務對數據庫進行修改時,InnoDB 會生成對應的 undo log;如果事務執行失敗或調用了 rollback,導致事務需要回滾,便可以利用 undo log 中的信息將數據回滾到修改之前的樣子。
undo log 屬於邏輯日誌,它記錄的是 sql 執行相關的信息。當發生回滾時,InnoDB 會根據 undo log 的內容做與之前相反的工作:

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

持久性

Innnodb 有很多 log,持久性靠的是 redo log。

一條 SQL 更新語句怎麼運行

持久性肯定和寫有關,MySQL 裏經常說到的 WAL 技術,WAL 的全稱是 Write-Ahead Logging,它的關鍵點就是先寫日誌,再寫磁盤。就像小店做生意,有個粉板,有個賬本,來客了先寫粉板,等不忙的時候再寫賬本。

redo log

redo log 就是這個粉板,當有一條記錄要更新時,InnoDB 引擎就會先把記錄寫到 redo log(並更新內存),這個時候更新就算完成了。在適當的時候,將這個操作記錄更新到磁盤裏面,而這個更新往往是在系統比較空閒的時候做,這就像打烊以後掌櫃做的事。
redo log 有兩個特點

對於 redo log 是有兩階段的:commit 和 prepare
如果不使用 “兩階段提交”,數據庫的狀態就有可能和用它的日誌恢復出來的庫的狀態不一致.
好了,先到這裏,看看另一個。

Buffer Pool

InnoDB 還提供了緩存,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 宕機而丟失,從而滿足了持久性要求。
而且這樣做還有兩個優點:

binlog

說到這,可能會疑問還有個 bin log 也是寫操作並用於數據的恢復,有啥區別呢。

binlog 和 redo log

對於語句 update T set c=c+1 where ID=2;

  1. 執行器先找引擎取 ID=2 這一行。ID 是主鍵,直接用樹搜索找到。如果 ID = 2 這一行所在數據頁就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,再返回。

  2. 執行器拿到引擎給的行數據,把這個值加上 1,N+1,得到新的一行數據,再調用引擎接口寫入這行新數據。

  3. 引擎將這行新數據更新到內存中,同時將這個更新操作記錄到 redo log 裏面,此時 redo log 處於 prepare 狀態。然後告知執行器執行完成了,隨時可以提交事務。

  4. 執行器生成這個操作的 binlog,並把 binlog 寫入磁盤。

  5. 執行器調用引擎的提交事務接口,引擎把剛剛寫入的 redo log 改成提交(commit)狀態,更新完成

爲什麼先寫 redo log 呢 ?

一致性

一致性是事務追求的最終目標,前問所訴的原子性、持久性和隔離性,其實都是爲了保證數據庫狀態的一致性。
當然,上文都是數據庫層面的保障,一致性的實現也需要應用層面進行保障。
也就是你的業務,比如購買操作只扣除用戶的餘額,不減庫存,肯定無法保證狀態的一致。

總結

MySQL 都很熟, ACID 也知道是個啥,但 MySQL 的 ACID 怎麼實現的?
有時候,就像你知道了有 undo log、redo log 但可能並不太清楚爲什麼有,當知道了設計的目的,瞭解起來就會更加清晰了。

參考

MVCC 實現原理
MySQL 中的鎖
MySQL 事務中 ACID 實現原理
深入 MySQL 事務

出處:https://llc687.top/131.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0zcC-E95eVUNcugteANt4w