【問題描述】
開發有天碰到一個很奇怪的問題,他的場景是這樣子的:
通過Canal來訂閱MySQL的binlog, 當捕獲到有資料變化時,回到資料庫,反查該資料的明細,然后做進一步處理,
有一次,他碰到一個詭異的現象:
1. Canal收到訊息,有一條主鍵id=31019319的資料插入
2. 11:19:51.081, 應用程式去反查資料庫,11:19:51.084查詢完畢,發現id=31019319的資料為空
3. 過幾分鐘后,開發去手工查資料庫,發現id=31010319的資料是存在的,每次插入的時候,我們會在資料庫記錄插入時間,發現插入的時間是11:19:51.059,
讓開發感到困惑的是11:19:51.059寫入的資料,11:19:51.081去查詢,應該是能查到資料的呀,我們首先排除了讀寫分離,主從分離等場景,Canal訂閱和資料庫查詢都是在Master上,所以這個問題就變得非常詭異了,
【問題分析】
因為中間夾雜著Canal, 而Canal是通過binlog讀取的,這個問題我們可以簡化為:當我們在master插入一條資料,該資料在master還沒落庫,但是在Slave卻能查到,我們嘗試重現這種場景,因為我們是采用GTID模式,GTID也就是全域事務編號,我們通過跟蹤GTID來除錯問題,
我們創建一個測驗表如下:
CREATE TABLE `gtid_debug` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
此時,在Master和Slave上,分別收集到的GTID資訊如下:
| 角色 | @@global_gtid_executed | @@port |
|---|---|---|
| Master | be7945f1-3613-11ec-8353-98039ba5775a:1-16 | 3306 |
| Slave | be7945f1-3613-11ec-8353-98039ba5775a:1-16 | 3307 |
我們在Master上開啟gdb除錯,在函式ReplSemiSyncMaster::commitTrx上設定斷點,
步驟1:
在Master上,開啟Session1, 插入一條資料:
insert into gtid_debug(name)values('test1');
此時會hit到斷點,
步驟2:
在Slave上,開啟Session2, 查看GTID:
| 角色 | @@global_gtid_executed | @@port |
|---|---|---|
| Slave | be7945f1-3613-11ec-8353-98039ba5775a:1-17 | 3307 |
也就是說,事務在Slave上,開始走字了,
我們進行如下查詢:可以看到,在Slave這條記錄能被查詢到,
slave>select * from test.gtid_debug;
| ID | NAME |
| ---- | ----- |
| 1 | test1 |
步驟3:
在Master上,我們開啟Session3, 查看GTID, 這個session也會被斷點中斷,我們繼續執行下一步,直到查詢結果回傳,注意,此時Session1還停留在斷點上,未提交成功,
| 角色 | @@global_gtid_executed | @@port |
|---|---|---|
| Master | be7945f1-3613-11ec-8353-98039ba5775a:1-16 | 3306 |
并進行如下查詢,回傳結果為空:
master>select * from test.gtid_debug;
Empty set
所以我們重現了問題,也就是說,在Master插入資料,事務還沒有提交,但在Slave就能查到了, Slave跑的比Master還快,
【原因分析】
重現了問題后,我們對問題進行分析,并查看了相應代碼,發現是半同步復制的模式導致,半同步復制有兩種模式: After_Sync(5.7版本默認)模式和After_Commit(5.6版本默認)模式,我們線上的版本是5.7,所以采用的是After_Sync模式,

從上圖可以看到,一個事務在半同步模式下提交,無論是after_sync還是after_commit,都要經歷4個階段:
1. InnoDB Redo File Prepare Write
2. Binlog File Flush & Sync
3. InnoDB Redo File Commit (同時釋放事務持有的鎖)
4. Send binlog to Slave
After_Commit模式的四個階段順序為: 1->2->3->4, 而after_sync模式的順序為1->2->4->3.
在5.7默認的after_sync模式下,確實存在先發送binlog到Slave, 然后再進行事務提交的場景,這時候大家會問了,為啥5.7把半同步復制改為after_sync模式了?這主要是因為after_commit機制存在資料丟失的風險,我們可以設想一下,在3->4的T1時間段,新資料對其它Session已經可見,突然Master掛了,MySQL進行主從切換,這時事務在Master上完成,如在Slave上不存在,切換后,業務會發現之前能查到的資料又沒了,
而在after_sync模式下,其執行的順序為1->2->4->3. 也就是說Master在收到Slave的應答之后,才Commit事務,在3->4的T1時間段內,因事務還未Commit,新資料對其它Session還不可見,所以看上去像比Slave跑的更慢,具體可以參考網上關于這兩種模式的討論,
【解決建議】
我們分析清楚問題之后,解決的方法就比較簡單了,不建議改為after_commit模式,雖然改為after_commit模式,可以保證事務在Master落地后,Canal才會讀到訊息,但存在主從切換事務丟失的風險,我們的解決方法,是在Canal訊息處理時,延后1秒再處理,這樣解決方法比較合理,因為一般來講,業務對訊息的實時性不是特別高,
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/548495.html
標籤:MySQL
上一篇:超詳細【入門精講】資料倉庫原理&實戰 一步一步搭建資料倉庫 內附相應實驗代碼和鏡像資料和腳本
下一篇:MySQL學習筆記-存盤引擎
