MySQL事務還沒提交,Canal就能讀到訊息了?

攜程DBA發表於2023-03-28

【問題描述】

開發有天碰到一個很奇怪的問題,他的場景是這樣子的:
透過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秒再處理。這樣解決方法比較合理。因為一般來講,業務對訊息的實時性不是特別高。

相關文章