開發有天碰到一個很奇怪的問題,他的場景是這樣子的:
通過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上設定斷點。
在Master上,開啟Session1, 插入一條資料:
insert into gtid_debug(name)values('test1');
此時會hit到斷點。
在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 |
在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秒再處理。這樣解決方法比較合理。因為一般來講,業務對訊息的實時性不是特別高。