- MySQL高可用實踐
- 王雪迎
- 3505字
- 2021-03-26 23:06:33
3.1 GTID簡介
3.1.1 什么是GTID
全局事務標識符GTID的英文全稱為Global Transaction Identifier,是在整個復制環境中對一個事務的唯一標識。它是MySQL 5.6加入的一個強大特性,目的在于能夠實現主從自動定位和切換,而不像以前需要指定文件和位置。使用GTID復制時,在主庫上提交事務時創建事務對應的GTID,從庫在應用中繼日志時用GTID識別和跟蹤每個事務。在啟動新從庫或因故障轉移到新主庫時,可以使用GTID來標識復制的位置,極大地簡化了這些任務。由于GTID的復制完全基于事務,因此只要在主庫上提交的所有事務也在從庫上提交,兩者之間的一致性就能得到保證。GTID支持基于語句或基于行的復制格式,但為了獲得最佳效果,MySQL建議使用基于行的格式。GTID始終保留在主庫和從庫上,這意味著可以通過檢查它的二進制日志來確定應用源于哪一個從庫的何種事務。而且,一旦在指定庫上提交了具有給定GTID的事務,則該庫將忽略具有相同GTID的任何后續事務。因此,在主庫上提交的事務只會在從庫上應用一次,這也有助于保證一致性。
3.1.2 GTID的格式與存儲
1. 單個GTID
GTID與主庫上提交的每個事務相關聯。此標識符不僅對發起事務的庫是唯一的,而且在給定復制拓撲結構中的所有庫中都是唯一的。GTID是由冒號分隔的一對坐標來表示的,例如:
8eed0f5b-6f9b-11e9-94a9-005056a57a4e:23
前一部分是主庫的server_uuid,后面一部分是主庫上按提交事務的順序確定的序列號,提交的事務序號從1開始。上面的GTID表示:具有8eed0f5b-6f9b-11e9-94a9-005056a57a4e的服務器上提交的第23個事務具有此GTID。MySQL 5.6后使用自動生成的128位server_uuid以避免沖突。數據目錄下的auto.cnf文件用來保存server_uuid。MySQL啟動的時候會讀取auto.cnf文件,如果沒有讀取到則會生成一個server_id,并保存到auto.cnf文件中。
在主庫上提交客戶端事務時,如果事務已寫入二進制日志,則會為其分配新的GTID,保證為客戶事務生成單調遞增且沒有間隙的GTID。如果未將客戶端事務寫入二進制日志(例如,因為事務已被過濾掉,或者事務是只讀的),則不會在源服務器上為其分配GTID。從庫上復制的事務保留與主庫上事務相同的GTID。即使從庫上未開啟二進制日志,GTID也會被保存。MySQL系統表mysql.gtid_executed用于保存MySQL服務器上應用的所有事務的GTID,但存儲在當前活動二進制日志文件中的事務除外。
GTID的自動跳過功能意味著一旦在指定服務器上提交了具有給定GTID的事務,則該服務器將忽略使用相同GTID執行的任何后續事務(這種情況是可能發生的,如手工設置了gtid_next時)。這有助于保證主從一致性,因為在主庫上提交的事務在從庫上應用不超過一次。如果具有給定GTID的事務已開始在服務器上執行但尚未提交或回滾,則任何在該服務器上啟動具有相同GTID的并發事務都將被阻止。服務器既不執行并發事務也不將控制權返回給客戶端。一旦先前的事務提交或回滾,就可以繼續執行在同一個GTID上被阻塞的并發會話。如果是回滾,則一個并發會話繼續執行事務,并且在同一個GTID上阻塞的任何其他并發會話仍然被阻止。如果是提交,則所有并發會話都將被阻止,并自動跳過事務的所有語句。mysqlbinlog輸出中的GTID_NEXT包含事務的GTID,用于標識復制中的單個事務。
下面做三個簡單實驗來驗證GTID的自動跳過功能。
實驗1:驗證自動跳過
(1)準備初始數據:
use test; create table t1(a int); create table t2(a int); insert into t1 values(1),(2); insert into t2 values(1),(2); commit;
(2)查看當前GTID:

(3)將GDIT設置為已經執行過的值,再執行事務:

可以看到,服務器已經執行了GTID為356的事務,后續相同GTID的事務都被自動跳過,雖然truncate語句沒有報錯,但并未執行,數據無變化。
實驗2:驗證兩個相同GTID事務,事務1提交,事務2被跳過
(1)準備兩個SQL腳本s1.sql和s2.sql,gtid_next是一個沒用過的新值。
s1.sql內容如下:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:357'; begin; delete from test.t1 where a=1; select sleep(10); commit; set gtid_next=automatic;
s2.sql內容如下:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:357'; begin; delete from test.t2 where a=1; commit; set gtid_next=automatic;
(2)在會話1執行s1.sql,并且在其sleep期間,在會話2執行s2.sql:
-- 會話1 mysql -uroot -p123456 test < s1.sql -- 會話2 mysql -uroot -p123456 test < s2.sql
(3)查詢數據:

可以看到,事務1提交前,事務2被阻塞。事務1提交后,具有相同GTID的事務2被跳過。
實驗3:驗證兩個相同GTID事務,事務1回滾,事務2提交
(1)準備兩個SQL腳本s1.sql和s2.sql,gtid_next是一個沒用過的新值。
s1.sql內容如下:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:360'; begin; delete from test.t1 where a=2; select sleep(10); rollback; set gtid_next=automatic;
s2.sql內容如下:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:360'; begin; delete from test.t2 where a=1; commit; set gtid_next=automatic;
(2)在會話1執行s1.sql,并且在其sleep期間,在會話2執行s2.sql:
-- 會話1 mysql -uroot -p123456 test < s1.sql -- 會話2 mysql -uroot -p123456 test < s2.sql
(3)查詢數據:

可以看到,事務1回滾前,事務2被阻塞。事務1回滾后,具有相同GTID的事務2被提交。
2. GTID集
GTID集是包括一個或多個單個GTID或GTID范圍的集合。源自同一個服務器的一系列GTID可以折疊為單個表達式,例如:
8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-321
上面的示例表示源自server_uuid為8eed0f5b-6f9b-11e9-94a9-005056a57a4e服務器的第1到第321個事務。源自同一個服務器的多個單GTID或GTID范圍可以同時包含在由冒號分隔的單個表達式中,例如:
8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-3:11:47-49
GTID集可以包括單個GTID和GTID范圍的任意組合,甚至它可以包括源自不同服務器的GTID。例如一個存儲在從庫gtid_executed系統變量中的GTID集可能如下:
565a6b0a-6f05-11e9-b95c-005056a5497f:1-20, 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-321
表示該從庫已從兩個主庫應用了事務,也有可能是在從庫執行的寫操作。當從庫變量返回GTID集時,UUID按字母順序排列,并且數值間隔按升序合并。
MySQL服務器中很多地方都用到GTID集,例如:gtid_executed和gtid_purged系統變量存儲的值是GTID集;START SLAVE的UNTIL SQL_BEFORE_GTIDS和UNTIL SQL_AFTER_GTIDS子句的值是GTID集;內置函數GTID_SUBSET()和GTID_SUBTRACT()需要GTID集作為輸入等。
3. mysql.gtid_executed表
mysql.gtid_executed表結構如下:

mysql.gtid_executed表記錄的是服務器上已經執行事務的GTID。三個字段分別表示發起事務的服務器UUID、UUID集的起始和結束事務ID。對于單個GTID,后兩個字段的值相同。
mysql.gtid_executed表供MySQL服務器內部使用。當從庫禁用二進制日志時用該表記錄GTID,或者當二進制日志丟失時,可從該表查詢GTID狀態。RESET MASTER命令將重置mysql.gtid_executed表,清空表數據。和所有系統表一樣,用戶不要修改該表。
僅當gtid_mode設置為ON或ON_PERMISSIVE時,GTID才存儲在mysql.gtid_executed表中。存儲的GTID值取決于是否啟用二進制日志:
- 對于從庫,如果禁用了二進制日志記錄(skip-log-bin)或log_slave_updates,則服務器將在該表中存儲每個事務的GTID。
- 如果啟用了二進制日志記錄,當刷新二進制日志或重啟服務器時,服務器都會將當前二進制日志中所有事務的GTID寫入mysql.gtid_executed表。這種情況適用于主庫或啟用了二進制日志記錄的從庫。
啟用二進制日志記錄時,mysql.gtid_executed表并不保存所有已執行事務GTID的完整記錄,該信息由gtid_executed全局系統變量的值提供,每次提交事務后更新。如果服務器意外停止,則當前二進制日志文件中的GTID集不會保存在mysql.gtid_executed表中。在MySQL實例恢復期間,這些GTID將從二進制日志文件添加到表中。即使服務器處于只讀模式,MySQL服務器也可以寫入mysql.gtid_executed表,這樣二進制日志文件仍然可以在只讀模式下輪轉。如果無法訪問mysql.gtid_executed表時進行二進制日志文件輪轉,則繼續使用二進制日志文件存儲GTID,同時在服務器上記錄警告信息:

前面已經提到,mysql.gtid_executed表的記錄可能并不是完整的已執行GTID,而且有不可訪問的可能性,例如誤刪除此表,因此建議始終通過查詢@@global.gtid_executed來確認MySQL服務器的GTID狀態,而不是查詢mysql.gtid_executed表。mysql.gtid_executed表可能隨著事務量的增多而快速膨脹,存儲了源自同一個服務器的大量不同的單個GTID,這些GTID構成一個范圍,例如:

為了節省空間,MySQL服務器定期壓縮mysql.gtid_executed表,方法是將每個這樣的行集替換為跨越整個事務標識符間隔的單行,如下所示:

通過設置gtid_executed_compression_period系統變量,可以控制壓縮表之前允許的事務數,從而控制壓縮率。此變量的默認值為1000,指的是在每1000次事務之后執行表的壓縮。把gtid_executed_compression_period設置為0,將不執行壓縮。注意,啟用二進制日志時不使用gtid_executed_compression_period的值,并在每個二進制日志輪轉時壓縮mysql.gtid_executed表。mysql.gtid_executed表的壓縮由名為thread/sql/compress_gtid_table的專用前臺線程執行。此線程未在SHOW PROCESSLIST的輸出中列出,但可以從performance_schema.threads中查詢到:

通常該線程都處于暫停狀態,只有當滿足條件時被喚醒,如達到gtid_executed_compression_period或發生了二進制日志輪轉(如flush logs等)時。
下面做個簡單實驗展示一下reset master的作用和影響。
(1)查看從庫當前已經執行的GTID和二進制日志:
show master status\G show variables like 'gtid%'; select * from mysql.gtid_executed; show slave status\G
(2)查詢結果如下:

所有查詢顯示已經執行的GTID均為8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-6。
查看當前的binlog結果如下:

當前從庫有4個binlog文件。
(1)在從庫執行reset master。
(2)再次執行(1)的查詢。可以看到所有查詢的gtid_executed都置空,binlog文件只有binlog.000001一個。說明reset master命令會清空gtid_executed變量和mysql.gtid_executed表,并會只保留一個初始的binlog文件。
(3)在主庫上執行一些更新。
use test; create table t1(a int); insert into t1 select 1;
(4)再次執行(1)的查詢。可以看到mysql.gtid_executed表中沒有記錄,其他查詢都已顯示出新執行GTID的值,復制正常。說明mysql.gtid_executed不記錄當前binlog中的GTID。
(5)從庫執行flush logs。在從庫上執行flush logs后,mysql.gtid_executed表中存儲了從reset master到flush logs之間binlog中的GTID。
從以上步驟看到,從庫上執行reset master只是清空從庫的gtid_executed,隨著復制的繼續,其gtid_executed的值也將隨之變化,對復制和主從數據一致性沒有影響。下面繼續實驗,看一下在主庫上執行reset master會產生哪些影響。
(6)在主庫上執行以下語句:

(7)在上一步執行期間,開啟一個新會話在主庫上執行reset master。
(8)查看從庫的復制狀態。從show slave status的輸出中可以看到復制的I/O線程已停止,并報以下錯誤:

由于主庫正在執行事務中間進行了reset master,從庫無法讀取主庫的二進制日志而報錯。更有甚之,這些二進制日志的丟失是永久性的,結果很可能需要從頭重建復制。由此實驗得出的結論是,作為一條基本原則,不要隨意在主庫上執行reset master,這樣做極有可能導致復制停止或造成主從數據不一致等嚴重后果,而且不易恢復。
- Spring Boot 2實戰之旅
- Julia機器學習核心編程:人人可用的高性能科學計算
- Mastering LibGDX Game Development
- C++程序設計基礎教程
- Learning Network Forensics
- Visual Studio 2015高級編程(第6版)
- 移動互聯網軟件開發實驗指導
- Android移動開發案例教程:基于Android Studio開發環境
- 匯編語言編程基礎:基于LoongArch
- R語言:邁向大數據之路(加強版)
- 數字媒體技術概論
- 從零開始學Unity游戲開發:場景+角色+腳本+交互+體驗+效果+發布
- ASP.NET Core and Angular 2
- C# 7 and .NET Core 2.0 Blueprints
- MATLAB計算機視覺實戰