全網最牛 X 的!MySQL 兩階段提交串講!沒有之一!

Hi,大家好!我是白日夢!

今天我要跟你分享的 MySQL 話題是:“全網最牛 X 的!MySQL 兩階段提交串講!沒有之一!”

本文是 MySQL 專題的第 23 篇。

歡迎關注!持續更新中!

目錄:

一、吹個牛

面試官的一句:“瞭解 MySQL 的兩階段提交嗎?” 不知道問涼了多少人!

這篇文章白日夢就和大家分享什麼是 MySQL 的兩階提交到底是怎麼回事!不管你原來曉不曉得兩階段提交,相信我!這篇文章中你一定能 get 到新的知識!

在說兩階段提交之前,白日夢用了大量的篇幅再講 undo-log、redo-log、binlog。

先了解它們,才能更好的理解什麼是兩階段提交,如果你如果還沒有看,推薦你去翻一翻前面的專題文章。

二、事務及它的特性

在說兩階段提交事物之前,我們先來說說事務。

一般當我們的功能函數中有批量的增刪改時,我們會添加一個事物包裹這一系列的操作,要麼這一組操作全部執行成功,只要有一條 SQL 執行失敗了我們就全部回滾。相信你一定聽說過這個比較經典的轉賬的 Case。有一定工作經驗的同學都知道,這麼做其實是保護我們的數據庫中不出現髒數據。整體數據會變的可控。

對 MySQL 來說你可以通過下面的命令顯示的開啓、提交、回滾事務

# 開啓事務
begin;

# 或者下面這條命令
start transaction;

# 提交
commit;

# 回滾
rollback;

但是日常開發中大家普遍使用編程語言操作數據庫。比如 Java、Golang... 在使用這種具體編程語言持久層的框架時,它們一般都支持事務操作,比如:在 Spring 中你可以對一個方法添加註解@Transctional顯示的開啓事務。Golang 的 beego 中也提供了讓你可以顯示的開啓事務的函數。

有一點不太好的地方是:大家在享受這種編程框架帶來的便利的同時,它也屏蔽了你對 MySQL 事務認知。讓人們懶得去往細了看事務

你可以往看我下面這個很簡單的 Case。

我有一張數據表

CREATE TABLE `test_backup` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

然後我往這個表中 insert 幾條數據

mysql> insert into test_backup values(1,'tom');
mysql> insert into test_backup values(2,'jerry');
mysql> insert into test_backup values(1,'herry');

再去查看 binlog。

你會不會詫異?我上面明明沒有顯示的添加 begin、commit 命令,但是 MySQL 實際執行我的 SQL 時,竟然爲我添加上了!

原因很簡單:跟大家分享一個參數如下:

一般大家的線上庫都會將這個參數置爲 ON,你的 SQL 會自動的開啓一個事物,並且 MySQL 會自動的幫你把它提交。

也就是說: 當這個參數爲 ON 時,你使用的 DAO 持久層框架發送給數據庫的 SQL 其實都會被放在一個事物中執行,然後這個事物被自動提交,而我們對這個過程是無感知的。具體一點,比如你使用某框架的@Transctional註解,或者在 golang 中可以像下面的方式獲得一個事物:

db := mysql.Client
ops := &sql.TxOptions{
  Isolation: 0,
  ReadOnly:  false,
 }
tx, err := db.BeginTx(ctx, ops)
// todo with tx

然後你所有的操作都放在這個事物中執行。

這時你使用的持久層框架肯定會向 MySQL 發送一條命令:begin; 或者是 start transcation; 來保證你這一組 SQL 中執行一條 SQL 後,開啓的事物不會被 MySQL 自動幫你提交了。

其實還是推薦將這個參數設置成ON的,當然你也可以像下面這樣將它關閉

mysql> set autocommit = 0;

但是關閉它之後,MySQL 不會幫你自動提交事物,全靠研發同學自己來維護就容易會出現長事物,在內存中產生一個極其長的 undo log 鏈條。壞處多多。

關於長事物,你可以看白日夢的這篇筆記:大話 undo log

三、簡單看下兩階段提交的流程

瞭解了什麼是事物,再來看下什麼是兩階段提交。其實所謂的兩階段就是把一個事物分成兩個階段來提交。就像下圖這樣。

上圖爲兩階段提交的時序圖。

你可以粗略的觀察一下上圖,MySQL 想要準備事務的時候會先寫 redolog、binlog 分成兩個階段。

兩階段提交的第一階段 (prepare 階段):寫 rodo-log 並將其標記爲 prepare 狀態。

緊接着寫 binlog

兩階段提交的第二階段(commit 階段):寫 bin-log 並將其標記爲 commit 狀態。

不瞭解這些日誌是什麼有啥用也沒關係,你可以先去看我之前的系列文章。

四、兩階段寫日誌用意?

你有沒有想過這樣一件事,binlog 默認都是不開啓的狀態!

也就是說,如果你根本不需要 binlog 帶給你的特性(比如數據備份恢復、搭建 MySQL 主從集羣),那你根本就用不着讓 MySQL 寫 binlog,也用不着什麼兩階段提交。

只用一個 redolog 就夠了。無論你的數據庫如何 crash,redolog 中記錄的內容總能讓你 MySQL 內存中的數據恢復成 crash 之前的狀態。

所以說,兩階段提交的主要用意是:爲了保證 redolog 和 binlog 數據的安全一致性。只有在這兩個日誌文件邏輯上高度一致了。你才能放心的使用 redolog 幫你將數據庫中的狀態恢復成 crash 之前的狀態,使用 binlog 實現數據備份、恢復、以及主從複製。而兩階段提交的機制可以保證這兩個日誌文件的邏輯是高度一致的。沒有錯誤、沒有衝突。

當然,兩階段提交能做到足夠的安全還需要你合理的設置 redolog 和 binlog 的 fsync 的時機,而這塊知識點所涉及到的參數前幾篇文章已經說過。如果不記得,可以去看下。

五、加餐:sync_binlog = 1 問題

如果你看懂了我下面說的這些話,能幫你更好的理解兩階段提交哦!純乾貨!

白日夢在前面的分享 binlog 的文章中有跟大家提到過一個參數sync_binlog=1。這個參數控制 binlog 的落盤時機,並且白日夢也知道你們公司線上數據庫的該參數一定被設置成了 1。

我在寫那篇 binlog 文章之前,就計劃好寫這篇文章了。白日夢的 MySQL 在動筆之前已經列好了大綱,從簡單到複雜,從 0 到 1 開始更新,歡迎小夥伴們關注我,持續更新中~

Notice!!!這個參數爲 1 時,表示當事物提交時會將 binlog 落盤。

現在你用 15s 中的時間,思考一下,藍色句子中說的事物提交時會將 binlog 落盤,這個提交時,是下圖中的 step1 時刻呢?還是 step2 時刻呢?

答案是:step1 時刻!

知道這個知識點很重要,下面我來描述這樣一個場景。

假如要執行一條 update 語句,那你肯定知道,先寫 undolog(便於後續對 update 事務的回滾)。然後你的 update 邏輯將 Buffer Pool 中的緩存頁修改成了髒頁。

當你準備提交事物時(也就是 step1 階段),會寫 redolog,並將其標記爲 prepare 階段。然後再寫 binlog,並將 binlog 落盤。

這時發生了意外,MySQL 宕機了。

那我問你,當你重啓 MySQL 後,update 對 BufferPool 中做出的修改是會被回滾還是會被提交呢?

答案是:會根據 redolog 將修改後的 recovey 出來,然後提交。

那爲什麼會這樣做呢?

其實總的來說,不論 mysql 什麼時刻 crash,最終是 commit 還是 rollback 完全取決於 MySQL 能不能判斷出 binlog 和 redolog 在邏輯上是否達成了一致。只要邏輯上達成了一致就可以 commit,否則只能 rollback。

比如還是上面描述的場景,binlog 已經寫了,如果 MySQL 最終選擇了回滾。那代表你的 binlog 比 BufferPool(或者 Disk)中的真實數據多出一條更新,日後你用這份 binlog 做數據恢復,是不是結果一定是錯誤的?

或者說 binlog 已經落盤了,那很有可能所有的從庫都把這條 binlog 拿走自己回放了,這時如果主庫選擇回滾丟棄這條數據,那是不是主從數據不一致了?

六、如何判斷 binlog 和 redolog 是否達成了一致

這個知識點可是純乾貨!

當 MySQL 寫完 redolog 並將它標記爲 prepare 狀態時,並且會在 redolog 中記錄一個 XID,它全局唯一的標識着這個事務。而當你設置 sync_binlog=1 時,做完了上面第一階段寫 redolog 後,mysql 就會對應 binlog 並且會直接將其刷新到磁盤中。

下圖就是磁盤上的 row 格式的 binlog 記錄。binlog 結束的位置上也有一個 XID。

只要這個 XID 和 redolog 中記錄的 XID 是一致的,MySQL 就會認爲 binlog 和 redolog 邏輯上一致。就上面的場景來說就會 commit,而如果僅僅是 rodolog 中記錄了 XID,binlog 中沒有,MySQL 就會 RollBack

七、兩階段提交設計的初衷 - 分佈式事務

其實兩階段提交更多的被使用在分佈式事務的場景。

我用大白話描述一個這樣的場景,大家自行腦補一下:

MySQL 單機本來是支持事務的,但是這裏所謂的分佈式事務實際上指的是跨數據庫、跨集羣的事務。比如說你公司的業務太火爆了,每天都產生大量的數據,這些數據不僅單表存不下,甚至單庫都存不下了(已經達到了服務器硬件存儲的瓶頸)

那你怎麼辦?是不是隻能將單庫拆分成多庫?

那你拆分成多庫就會面臨這樣一個新的問題。假設 Tom 給 Jerry 轉賬,但是由於你拆分了數據庫,原本在同庫同表上的 Tom 和 Jerry 的信息,被你拆分進 A 庫 a 表和 B 庫 b 表。那你再發起轉賬邏輯時,萬一失敗了。如何回滾保證數據的安全?這就是分佈式事務的要解決的問題。

通常各大公司都有自己的支持分佈式事務中間件,中間件的作用本質上就是處理好各個數據庫節點之間兩階段提交的問題。

簡單來說:就是中間件要協調各個數據節點。

第一階段:中間件告訴各數據庫節點,讓它們開啓 XA 事務,然後判斷所有數據庫節點是否已經處於 prepare 狀態

第二階段:中間件判斷事務提交還是回滾的階段。如果所有節點都 prepare 那就統一提交。但凡出現一個失敗的節點,統一回滾。

這裏只是稍微提及一下:兩階段提交和分佈式事務的淵源。

白日夢後續計劃還會有文章中進一步跟大家詳細的分享分佈式事務話題。

八、再看 MySQL 兩階段寫日誌

那我們再將思路拉回到 MySQL 兩階段寫日誌的話題。

其實說到這裏,你大概也能直接想到,其實上一篇文章中的兩階段提交,表面上其實就是兩階段寫入日誌。

通過我前面的描述,你也一定知道了兩份日誌文件邏輯對齊的標記是有一份相同的 XID。

就是這種兩階段的機制保證了兩個日誌(在分佈式事務中就是多個數據節點)在邏輯上能達到一致的效果。

九、留一個彩蛋

如果你仔細想一下,上面第三部分在分享 sync_binlog=1  加餐時,我所描述的示例場景其實是適用於單機 MySQL 的簡單場景。

其實這個場景還能再複雜一些!

串聯 MySQL 集羣、將同步、半同步、異步的主從複製關係以及這裏的兩階段提交、日誌的落盤時機、幽靈事務!結合成一個場景效果會更好。

但是我將它放在《爲研發同學定製的面試指南》排期的後半部分也就是 MySQL 集羣部分。讓我們從易到難過度過去!歡迎關注白日夢。

十、推薦閱讀

1、MySQL 的修仙之路,圖文談談如何學 MySQL、如何進階!

2、數據庫面經,常見的面試題

3、談談 MySQL 中基數是什麼?

4、聊聊什麼是慢查?如何監控?如何排查?

5、對 Not Null 字段插入 Null 值有啥現象?

6、能談談 year、date、datetime、time、timestamp 的區別嗎?

7、你有沒有搞混查詢緩存和 Buffer Pool?談談看!

8、你知道數據庫緩衝池中的 LRU-List 嗎?

9、瞭解 InnoDB 的 FreeList 嗎?談談看!

10、瞭解 Flush-List 嗎?順便說一下髒頁的落盤機制!

11、用 11 張圖講清楚,當你 CRUD 時 BufferPool 中發生了什麼!以及 BufferPool 的優化!

12、瞭解 MySQL 的表空間 和 數據表嗎?談談看!

13、瞭解 MySQL 的數據行嗎?行溢出機制呢?談談看!

14、瞭解 MySQL 數據頁嗎?說說什麼是頁分裂吧!

15、用一分鐘瞭解 fsync 這個系統調用

16、簡述 undo log、truncate、以及 undo log 如何幫你回滾事務?

17、我勸!這位年輕人不講 MVCC,耗子尾汁!

18、傳說中的 MySQL 的 redo log 是什麼?談談看!

19、LSN、Checkpoint?談談 MYSQL 的崩潰恢復是怎麼回事!

20、MySQL 的 bin log 有啥用?在哪裏?誰寫的?怎麼配置?

21、bin log 有哪些格式?有啥區別?優缺點?線上用哪種格式?

22、刪庫後!除了跑路還能幹嘛?

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