MySQL讀寫分離,寫完讀不到問題如何解決
點擊上方"程序員歷小冰",選擇“置頂或者星標”
? ?你的關注意義重大!
大家好,我是歷小冰。
今天我們來詳細了解一下主從同步延遲時讀寫分離發(fā)生寫后讀不到的問題,依次講解問題出現(xiàn)的原因,解決策略以及 Sharding-jdbc、MyCat 和 MaxScale 等開源數(shù)據(jù)庫中間件具體的實現(xiàn)方案。
寫后讀不到問題
MySQL 經(jīng)典的一主兩從三節(jié)點架構是大多數(shù)創(chuàng)業(yè)公司初期使用的主流數(shù)據(jù)存儲方案之一,主節(jié)點處理寫操作,兩個從節(jié)點處理讀操作,分攤了主庫的壓力。
但是,有時候可能會遇到執(zhí)行完寫操作后,立刻去讀發(fā)現(xiàn)讀不到或者讀到舊狀態(tài)的尷尬場景。這是由于主從同步可能存在延遲,在主節(jié)點執(zhí)行完寫操作,再去從節(jié)點執(zhí)行讀操作,讀取了之前舊的狀態(tài)。

上圖展示了此類問題出現(xiàn)的操作順序示意圖:
?客戶端首先通過代理向主節(jié)點 Master 進行了寫入操作?緊接著第二步去從節(jié)點 Slave A 執(zhí)行讀操作,此時 Master 和 Slave A 之間的同步還未完成,所以第二步的讀操作讀取到了舊狀態(tài)?當?shù)谖宀皆俅芜M行讀操作時,此時同步已經(jīng)完成,所以可以從 Slave B 中讀取到正確的狀態(tài)。
下面,我們就來看一下為什么會出現(xiàn)此類問題。
MySQL 主從同步
理解問題背后發(fā)生的原因,才能更好的解決問題。MySQL 主從復制的過程大致如下圖所示,本篇文章只講解同步過程中的流程,建立同步連接和失聯(lián)重傳不是重點,暫不講解,感興趣的同學可以自行了解。

MySQL 主從復制,涉及主從兩個節(jié)點,一共四個四個線程參與其中:
主節(jié)點的 Client Thread,處理客戶端請求的線程,執(zhí)行如圖所示的1~5步驟,2,3,4步驟是為了保證數(shù)據(jù)的一致性和盡量減少丟失,第三步驟時會通知 Dump Thread;
主節(jié)點的 Dump Thread,接收到 Client Thread 通知后,負責讀取本地的 binlog 的數(shù)據(jù),將 binlog 數(shù)據(jù),binlog 文件名 以及當前發(fā)送 binlog 的位置信息發(fā)送給從節(jié)點;
從節(jié)點的 IO Thread 負責接收 Dump Thread 發(fā)送的 binlog 數(shù)據(jù)和相關位置信息,將其追加到本地的 relay log 等文件中;
從節(jié)點的 SQL Thread 檢測到 relay log 追加了新數(shù)據(jù),則解析其內容(其實就是解析 binlog 文件的內容)為可以執(zhí)行的 SQL 語句,然后在本地數(shù)據(jù)執(zhí)行,并記錄下當前執(zhí)行的 relay log 位置。
上述是默認的異步同步模式,我們發(fā)現(xiàn),從主節(jié)點提交成功到從節(jié)點同步完成,中間間隔了6,7,8,9,10多個步驟,涉及到一次網(wǎng)絡傳輸,多次文件讀取和寫入的磁盤 IO 操作,以及最后的 SQL 執(zhí)行的 CPU 操作。
所以,當主從節(jié)點間網(wǎng)絡傳輸出現(xiàn)問題,或者從節(jié)點性能較低時,主從節(jié)點間的同步就會出現(xiàn)延遲,導致文章一開始提及的寫后讀不到的問題。在高并發(fā)場景,從節(jié)點一般要過幾十毫秒,甚至幾百毫秒才能讀到最新的狀態(tài)。
常見的解決策略
一般來講,大致有如下方案解決寫后讀不出問題:
?強制走主庫?判斷主備無延遲?等主庫位點或 GTID 方案
強制走主庫
強制走主庫方案最容易理解和實現(xiàn),它也是最常用的方案。顧名思義,它就是強制讓部分必須要讀到最新狀態(tài)的讀操作去主節(jié)點執(zhí)行,這樣就不會出現(xiàn)寫后讀不出問題。這種方案問題在于將一部分讀壓力給了主節(jié)點,部分破化了讀寫分離的目的,降低了整個系統(tǒng)的擴展性。
一般主流的數(shù)據(jù)庫中間件都提供了強制走主庫的機制,比如,在 sharding-jdbc 中,可以使用?Hint?來強制路由主庫。

它的原理就是在 SQL 語句前添加 Hint,然后數(shù)據(jù)庫中間件會識別出 Hint,將其路由到主節(jié)點。
下面,我們就來看一下如果要去從庫查詢,并且要避免過期讀的方案,并分析各個方案的優(yōu)缺點。
判斷主備無延遲
第二種方案是使用 show slave status 語句結果中的部分值來判斷主從同步的延遲時間:
> show slave status
*************************** 1. row ***************************
Master_Log_File: mysql-bin.001822
Read_Master_Log_Pos: 290072815
Seconds_Behind_Master: 2923
Relay_Master_Log_File: mysql-bin.001821
Exec_Master_Log_Pos: 256529431
Auto_Position: 0
Retrieved_Gtid_Set:
Executed_Gtid_Set:
.....
?seconds_behind_master,表示落后主節(jié)點秒數(shù),如果此值為0,則表示主從無延遲?Master_Log_File 和 Read_Master_Log_Pos,表示的是讀到的主庫的最新位點,Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是備庫執(zhí)行的最新位點。如果這兩組值相等,則表示主從無延遲?Auto_Position=1 ,表示使用了 GTID 協(xié)議,并且備庫收到的所有日志的 GTID 集合 Retrieved_Gtid_Set 和 執(zhí)行完成的 GTID 集合 Executed_Gtid_Set 相等,則表示主從無延遲。
在進行讀操作前,先根據(jù)上述方式來判斷主從是否有延遲,如果有延遲,則一直等待到無延遲后執(zhí)行。但是這類方案在判斷是否有延遲時存在著假陽和假陰的問題:
?判斷無延遲,其他延遲了。因為上述判斷是基于從節(jié)點的狀態(tài),當主節(jié)點的 Dump Thread 尚未將最新狀態(tài)發(fā)送給從節(jié)點的 IO SQL 時,從節(jié)點可能會錯誤的判斷自己和主節(jié)點無延遲。?判斷有延遲,但是讀操作讀取的最新狀態(tài)已經(jīng)同步。因為 MySQL 主從復制是一直在進行的,寫后直接讀的同時可能還有其他無關寫操作,雖然主從有延遲,但是對于第一次寫操作的同步已經(jīng)完成,所以讀操作已經(jīng)可以讀到最新的狀態(tài)。
對于第一個問題,需要使用主從復制的 semi-sync 模式,上文中講解介紹的是默認的異步模式,semi-sync 模式的流程如下圖所示:

?當主節(jié)點事務提交的時候,Dump Thread 把 binlog 發(fā)給從節(jié)點;?從節(jié)點的 IO Thread 收到 binlog 以后,發(fā)回給主節(jié)點一個 ack,表示收到了;?主節(jié)點的 Dump Thread 收到這個 ack 以后,再通知 Client Thread ,此時才能給客戶端返回執(zhí)行成功的響應。
這樣,寫操作執(zhí)行后,就確保從節(jié)點已經(jīng)讀取到主節(jié)點發(fā)送的 binglog 數(shù)據(jù),即 Master_Log_File、 Read_Master_Log_Pos 或 Retrieved_Gtid_Set 是最新的,這樣才能與執(zhí)行的相關數(shù)據(jù)進行對比,判斷是否有延遲。
可惜的是,上述 semi-sync 模式只需要等待一個從節(jié)點的ACK,所以一主多從的模式該方案將會無效。
雖然該方案有種種問題,但是對于一致性要求不那么高的場景也能適用,比如 MyCat 就是用 seconds_behind_master 是否落后主節(jié)點過多,如果超過一定閾值,就將其從有效從節(jié)點列表中刪除,不再將讀請求路由到它身上。
在 MyCAT 的用于監(jiān)聽從節(jié)點狀態(tài),發(fā)送心跳的 MySQLDetector 類中,它會讀取從節(jié)點的 seconds_behind_master,如果其值大于配置的 slaveThreshold,則將打印日志,并將延遲時間設置到心跳信息中。

下面,我們就介紹能夠解決第二個問題的方案,即判斷有延遲,但是讀操作讀取的特定最新狀態(tài)已經(jīng)同步。
等GTID 方案
首先介紹一下 GTID,也就是全局事務 ID,是一個事務在提交的時候生成的,是這個事務的唯一標識。它由MySQL 實例的uuid和一個整數(shù)組成,該整數(shù)由該實例維護,初始值是 1,每次該實例提交事務后都會加一。
MySQL 提供了一條基于 GTID 的命令,用于在從節(jié)點上執(zhí)行,等待從庫同步到了對應的 GTID(binlog文件中會包含 GTID),或者超時返回。

MySQL 在執(zhí)行完事務后,會將該事務的 GTID 會給客戶端,然后客戶端可以使用該命令去要執(zhí)行讀操作的從庫中執(zhí)行,等待該 GTID,等待成功后,再執(zhí)行讀操作;如果等待超時,則去主庫執(zhí)行讀操作,或者再換一個從庫執(zhí)行上述流程。
MariaDB 的 MaxScale 就是使用該方案,MaxScale 是 MariaDB 開發(fā)的一個數(shù)據(jù)庫智能代理服務(也支持 MySQL),允許根據(jù)數(shù)據(jù)庫 SQL 語句將請求轉向目標一個到多個服務器,可設定各種復雜程度的轉向規(guī)則。

MaxScale 在其 readwritesplit.hh 頭文件和 rwsplit_causal_reads.cc 文件中的 add_prefix_wait_gtid 函數(shù)中使用了上述方案。

舉個例子,原來要執(zhí)行讀操作的 SQL 和添加了前綴的 SQL 如下所示:

當 WAIT_FOR_EXECUTED_GTID_SET 執(zhí)行失敗后,原 SQL 就不會再執(zhí)行,而是將該 SQL 去主節(jié)點執(zhí)行。
后記
感覺大家一直讀到文末,后續(xù)小冰會繼續(xù)為大家奉上高質量的文章,也希望大家繼續(xù)關注。
-關注我
推薦閱讀
一萬字詳解 Redis Cluster Gossip 協(xié)議
為什么 ElasticSearch 比 MySQL 更適合復雜條件搜索
參考
??https://time.geekbang.org/column/article/77636?https://www.cnblogs.com/rickiyang/p/13856388.html?https://www.cnblogs.com/paul8339/p/7615310.html?https://github.com/mariadb-corporation/MaxScale
