[轉]MySQL 5.6 全局事務 ID(GTID)實現原理(三)


原文連接:http://qing.blog.sina.com.cn/1757661907/68c3cad333002s5l.html

原文作者:淘長源

轉載注明以上信息


 

這是 MySQL 5.6 全局事務 ID(GTID) 系列的第三篇博客。

 
在之前的兩篇博客中, 第一篇​ 介紹了全局事務 ID 的定義與數據結構。 第二篇​ 介紹了 MySQL 5.6 新增的全局事務狀態(Gtid_state)。
 
這里准備介紹的是全局事務 ID 如何參與 MySQL 的主備復制流程。
 
MySQL 5.6 引入全局事務 ID 的首要目的,是保證 Slave 在復制的時候不會重復執行相同的事務操作;其次,是用全局事務 IDs 代替由文件名和物理偏移量組成的復制位點,定位 Slave 需要復制的 binlog 內容。
 
因此,MySQL 必須在寫 binlog 時記錄每個事務的全局 GTID,保證 Master / Slave 可以根據這些 GTID 忽略或者執行相應的事務。在實現上,MySQL 沒有修改舊的 binlog 事件,而是新增了兩類事件:
 
+----------------------------+----------------------------------------+
| 名稱                                    | 功能                                                      |
+----------------------------+----------------------------------------+
| Previous_gtids_log_event | 該事件之前的全局事務 ID 集合。          |
+----------------------------+----------------------------------------+
|                  Gtid_log_event | 標記之后的事務對應的全局事務 ID。    |
+----------------------------+----------------------------------------+
 
Gtid_log_event
 
在 MySQL 5.6 的 binlog 文件中,每個事務的開始不是 "BEGIN" ,而是 Gtid_log_event 事件:
 
MySQL 5.6 全局事務 ID(GTID)實現原理(三)
(圖片來源: MySQL_Innovation_Day_Replication_HA.pdf​)
 
它里面只包含一條 GTID,記錄結構如下:
 
Gtid_log_event := (commit_flag, sid, gno)   // commit_flag 目前總是 true
 
里面 sid 就是產生該事務的 server_uuid,gno 是順序編號的 transaction_id。
 
把 Gtid 記錄在事務的開頭是為了便於 MySQL 過濾 binlog:檢查到某個 Gtid 不需要時,可以直接忽略后面的整段事務。
 
MySQL 5.6 保證同時寫入 Gtid_log_event 和全局 logged_gtids 狀態:
 
第一步,在向 binlog_cache_data 寫入第一條 binlog 前,MySQL 會在緩存的 buffer 中寫入一個空的 Gtid_log_event 占位。
 
第二步,當 binlog_cache_data 的內容刷到 binlog 文件時,MySQL 會把位於緩存 buffer 的 Gtid_log_event 內容替換成實際的 GTID,重新寫入緩存。
 
最后,MySQL 調用 Gtid_state 的 update_on_flush() 把 GTID 寫入 logged_gtids,再調用 sync_binlog_file()  保證內容更新到磁盤。 
 
在主備復制中,Slave 不像 Master 那樣自動產生 GTID,而是直接拷貝 Gtid_log_event 中包含的 GTID。這個特性是這樣實現的 —— MySQL 5.6 維護了一個線程(Session)級別的變量  gtid_next,類型為 Gtid_specification:
 
Gtid_specification := (enum_group_type, Gtid)
 
enum_group_type :=enum(AUTOMATIC_GROUP, GTID_GROUP, ANONYMOUS_GROUP, INVALID_GROUP, UNDEFINED_GROUP)
 
在 Master 執行事務時,gtid_next 的類型默認是 AUTOMATIC_GROUP,表示應該調用 generate_automatic_gno() 自動產生全局事務 ID。
 
而在 Slave 執行事務時,先用 Gtid_log_event 內的 Gtid 覆蓋 gtid_next,使它的類型為 GTID_GROUP。這樣的話,MySQL 會使用 gtid_next 內設置的 Gtid  值作為下一個全局事務 ID。
 
Previous_gtids_log_event
 
這個事件出現在 MySQL 5.6 每個 binlog 文件的開始處。
 
MySQL 創建一個新的 binlog 文件后,首先寫入一個 Format_description_log_event 描述,接着寫入一個 Previous_gtids_log_event,內容是在創建這個 binlog 文件之前執行的全局事務 GTIDs。 
 
事件的格式很簡單,就是字符串編碼的 Gdit_set:(編碼格式參考本文  第一篇​)
 
Previous_gtids_log_event := buffer of Gtid_set
(例如:3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5)
 
這個事件只是作為記錄。在主備復制時,Slave 會忽略 binlog 里的 Previous_gtids_log_event 事件。
 
Binlog 與持久化全局事務狀態
 
在  上一篇​ 沒有講到 MySQL 5.6 如何持久化全局事務狀態的 —— logged_gtids 和 lost_gtids 狀態里存儲了這台數據庫  有史以來 執行的所有 GTIDs(包括刪除 binlog 中的 GTIDs)—— 如果數據庫停機或崩潰前不做持久化,之后肯定丟失信息。
 
MySQL 的解決方案很簡單,在啟動時掃描剩余的 binlog 文件,用文件存儲的 Previous_gtids_log_event 和 Gtid_log_event 事件內容恢復全局 logged_gtids 和 lost_gtids 狀態。
 
具體的代碼如下:(源代碼:mysql-5.6.9-rc\sql\binlog.cc,line 2558)
 
第一步,找到最后一個 binlog 文件,讀出 Previous_gtids_log_event 記錄;再遍歷 binlog 文件中所有的 Gtid_log_event,  把找到的 GTID 記錄合並起來,作為這台數據庫歷史上執行的所有 GTIDs 放入全局 logged_gtids 記錄;
 
第二步,找到第一個 binlog 文件,用它的 Previous_gtids_log_event 信息代替全局 lost_gtids 的內容。因為這是第一個未刪除的 binlog 文件,這里記錄的就是之前已經刪除的 binlog 文件所包含的全部 GTIDs。
 
由於 MySQL 在提交事務中是最后才寫入真實 Gtid_log_event 信息的,從 binlog 恢復信息,可以保證讀到的 GTIDs 與成功執行的事務一致。
 
CHANGE MASTER TO ... 
 
MySQL 5.6 主備復制的一個改變,是新增了 COM_BINLOG_DUMP_GTID 協議,支持在 Slave 切換到新 Master 時,用 MASTER_AUTO_POSITION = 1 (auto_position 方式)代替原來的 binlog 文件名和物理偏移量。
 
COM_BINLOG_DUMP_GTID 協議並不復雜,請求格式如下:

 

Request = { server_id, binlog_name, binlog_offset, gtids_executed }  
 
如果采用 auto_position 方式連接 Master,現在 Slave 發送的 binlog_name 和 binlog_offset 都是空白,Master 只使用 gtids_executed 定位 Slave 上需要執行的 binlog。
 
實現邏輯是這樣的:Master 從第一個文件開始讀取 binlog,逐個檢查 Gdit_log_event 事件的全局事務 ID 是不是包含在 Slave 發送的 gtids_executed 集合中。如果發現這個 GTID 已經包含在 gtids_executed 集合內,就忽略后面的整段事務,不向 Slave 發送 binlog 內容。
 
其實這個過程還不是很優化,因為如果是正常情況,Master 需要遍歷若干 G 的 binlog 才能找到 Slave 需要復制的 binlog 內容 —— 這應該是一個改進點。
 
全局事務 ID 與並發復制
 
MySQL 5.6 主備復制的另一個改變,是實現了多線程並行復制。這個功能必須有全局事務 ID 的支持,原因是:
 
1) 在並行復制方式下,有些操作是不按照記錄在 binlog 中的順序執行的。這樣的話,如果按照文件名 + 物理偏移量的方式記錄復制位點,則停止 / 恢復主備復制時,可能會有一些操作被重復執行。
 
2) 我們知道,即使是 Mixed / Row 模式下記錄的 binlog,仍有些 DDL 操作是用 Statement 的方式編碼的,這些 DDL 操作不能在 Slave 重復執行(因為非冪等)。一旦操作在 Slave 執行出錯,結果就是復制中斷。
 
因此,Slave 必須依賴 binlog 中的全局事務 ID,在停止 / 恢復主備復制時,精確的記錄哪些事務在 Slave 執行過,哪些沒有。
 
現在,MySQL 5.6 可以用 COM_BINLOG_DUMP_GTID 來保證這一點:在恢復主備復制時,Slave 向 Master 發送自己所有執行過的 GTIDs(logged_gtids),在上次中斷主備復制時,已經執行過的 binlog 被 Master 直接濾掉,不向 Slave 傳送。
 
總結
 
在主備復制上,MySQL 5.6 新增了三個特性:
 
1)使用 GTIDs 作為主備復制的位點,在寫 binlog 時用 Gtid_log_event 標記事務。
 
2)支持 auto_position 方式進行主備切換。在新增的協議中,使用 GTIDs 作為復制位點向主庫請求 binlog 信息。
 
3)多線程並發復制,使用 GTIDs 防止事務重復執行。
 
全局事務 ID(GTID)可以很好的支持這幾個功能。而且,使用 GTIDs 避免了在傳送 binlog 邏輯上依賴文件名和物理偏移量,能夠更好的支持自動容災切換。
 
但是個人感覺,全局事務 ID 這里還有些待解決的問題:
 
1)GTID 是局部有序的,不能記錄事務的全局順序。因此在雙寫 / 快速主備切換場景下,不能根據 GTID 順序來解決更新沖突的問題。
 
2)容災切換時,MASTER_AUTO_POSITION 只能解決記錄位點的問題。為了保證一致性,停寫和等待主備 Caught up 仍然是必須的,通常這是服務無法快速恢復的主要原因。
 
補充:參考資料
 
這篇博客用到的參考資料:
 
MySQL 5.6 Manual:Replication with Global Transaction Identifiers( link​)

​​WL#4677: Unique Server Ids for Replication Topology (UUIDs)​​(link​)

WL#3584: Global Transaction Identifiers (GTIDs)( link​)

​​

順便提下, MySQL Worklog​ 是個好地方,你可以從這里了解 MySQL 的原始需求,開發人員的想法,還有值得關注的問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM