日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

28 | 读写分离有哪些坑?

發布時間:2023/12/10 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 28 | 读写分离有哪些坑? 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在上一篇文章中,我和你介紹了一主多從的結構以及切換流程。今天我們就繼續聊聊一主多從架構的應用場景:讀寫分離,以及怎么處理主備延遲導致的讀寫分離問題。

我們在上一篇文章中提到的一主多從的結構,其實就是讀寫分離的基本結構了。這里,我再把這張圖貼過來,方便你理解。

圖1 讀寫分離基本結構

讀寫分離的主要目標就是分攤主庫的壓力。圖1中的結構是客戶端(client)主動做負載均衡,這種模式下一般會把數據庫的連接信息放在客戶端的連接層。也就是說,由客戶端來選擇后端數據庫進行查詢。

還有一種架構是,在MySQL和客戶端之間有一個中間代理層proxy,客戶端只連接proxy, 由proxy根據請求類型和上下文決定請求的分發路由。

圖2 帶proxy的讀寫分離架構

接下來,我們就看一下客戶端直連和帶proxy的讀寫分離架構,各有哪些特點。

  • 客戶端直連方案,因為少了一層proxy轉發,所以查詢性能稍微好一點兒,并且整體架構簡單,排查問題更方便。但是這種方案,由于要了解后端部署細節,所以在出現主備切換、庫遷移等操作的時候,客戶端都會感知到,并且需要調整數據庫連接信息。
    你可能會覺得這樣客戶端也太麻煩了,信息大量冗余,架構很丑。其實也未必,一般采用這樣的架構,一定會伴隨一個負責管理后端的組件,比如Zookeeper,盡量讓業務端只專注于業務邏輯開發。

  • 帶proxy的架構,對客戶端比較友好。客戶端不需要關注后端細節,連接維護、后端信息維護等工作,都是由proxy完成的。但這樣的話,對后端維護團隊的要求會更高。而且,proxy也需要有高可用架構。因此,帶proxy架構的整體就相對比較復雜。

  • 理解了這兩種方案的優劣,具體選擇哪個方案就取決于數據庫團隊提供的能力了。但目前看,趨勢是往帶proxy的架構方向發展的。

    但是,不論使用哪種架構,你都會碰到我們今天要討論的問題:由于主從可能存在延遲,客戶端執行完一個更新事務后馬上發起查詢,如果查詢選擇的是從庫的話,就有可能讀到剛剛的事務更新之前的狀態。

    這種“在從庫上會讀到系統的一個過期狀態”的現象,在這篇文章里,我們暫且稱之為“過期讀”。

    前面我們說過了幾種可能導致主備延遲的原因,以及對應的優化策略,但是主從延遲還是不能100%避免的。

    不論哪種結構,客戶端都希望查詢從庫的數據結果,跟查主庫的數據結果是一樣的。

    接下來,我們就來討論怎么處理過期讀問題。

    這里,我先把文章中涉及到的處理過期讀的方案匯總在這里,以幫助你更好地理解和掌握全文的知識脈絡。這些方案包括:

    • 強制走主庫方案;
    • sleep方案;
    • 判斷主備無延遲方案;
    • 配合semi-sync方案;
    • 等主庫位點方案;
    • 等GTID方案。

    強制走主庫方案

    強制走主庫方案其實就是,將查詢請求做分類。通常情況下,我們可以將查詢請求分為這么兩類:

  • 對于必須要拿到最新結果的請求,強制將其發到主庫上。比如,在一個交易平臺上,賣家發布商品以后,馬上要返回主頁面,看商品是否發布成功。那么,這個請求需要拿到最新的結果,就必須走主庫。

  • 對于可以讀到舊數據的請求,才將其發到從庫上。在這個交易平臺上,買家來逛商鋪頁面,就算晚幾秒看到最新發布的商品,也是可以接受的。那么,這類請求就可以走從庫。

  • 你可能會說,這個方案是不是有點畏難和取巧的意思,但其實這個方案是用得最多的。

    當然,這個方案最大的問題在于,有時候你會碰到“所有查詢都不能是過期讀”的需求,比如一些金融類的業務。這樣的話,你就要放棄讀寫分離,所有讀寫壓力都在主庫,等同于放棄了擴展性。

    因此接下來,我們來討論的話題是:可以支持讀寫分離的場景下,有哪些解決過期讀的方案,并分析各個方案的優缺點。

    Sleep 方案

    主庫更新后,讀從庫之前先sleep一下。具體的方案就是,類似于執行一條select sleep(1)命令。

    這個方案的假設是,大多數情況下主備延遲在1秒之內,做一個sleep可以有很大概率拿到最新的數據。

    這個方案給你的第一感覺,很可能是不靠譜兒,應該不會有人用吧?并且,你還可能會說,直接在發起查詢時先執行一條sleep語句,用戶體驗很不友好啊。

    但,這個思路確實可以在一定程度上解決問題。為了看起來更靠譜兒,我們可以換一種方式。

    以賣家發布商品為例,商品發布后,用Ajax(Asynchronous JavaScript + XML,異步JavaScript和XML)直接把客戶端輸入的內容作為“新的商品”顯示在頁面上,而不是真正地去數據庫做查詢。

    這樣,賣家就可以通過這個顯示,來確認產品已經發布成功了。等到賣家再刷新頁面,去查看商品的時候,其實已經過了一段時間,也就達到了sleep的目的,進而也就解決了過期讀的問題。

    也就是說,這個sleep方案確實解決了類似場景下的過期讀問題。但,從嚴格意義上來說,這個方案存在的問題就是不精確。這個不精確包含了兩層意思:

  • 如果這個查詢請求本來0.5秒就可以在從庫上拿到正確結果,也會等1秒;

  • 如果延遲超過1秒,還是會出現過期讀。

  • 看到這里,你是不是有一種“你是不是在逗我”的感覺,這個改進方案雖然可以解決類似Ajax場景下的過期讀問題,但還是怎么看都不靠譜兒。別著急,接下來我就和你介紹一些更準確的方案。

    判斷主備無延遲方案

    要確保備庫無延遲,通常有三種做法。

    通過前面的第25篇文章,我們知道show slave status結果里的seconds_behind_master參數的值,可以用來衡量主備延遲時間的長短。

    所以第一種確保主備無延遲的方法是,每次從庫執行查詢請求前,先判斷seconds_behind_master是否已經等于0。如果還不等于0 ,那就必須等到這個參數變為0才能執行查詢請求。

    seconds_behind_master的單位是秒,如果你覺得精度不夠的話,還可以采用對比位點和GTID的方法來確保主備無延遲,也就是我們接下來要說的第二和第三種方法。

    如圖3所示,是一個show slave status結果的部分截圖。

    圖3 show slave status結果

    現在,我們就通過這個結果,來看看具體如何通過對比位點和GTID來確保主備無延遲。

    第二種方法,對比位點確保主備無延遲:

    • Master_Log_File和Read_Master_Log_Pos,表示的是讀到的主庫的最新位點;
    • Relay_Master_Log_File和Exec_Master_Log_Pos,表示的是備庫執行的最新位點。

    如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos這兩組值完全相同,就表示接收到的日志已經同步完成。

    第三種方法,對比GTID集合確保主備無延遲:

    • Auto_Position=1 ,表示這對主備關系使用了GTID協議。
    • Retrieved_Gtid_Set,是備庫收到的所有日志的GTID集合;
    • Executed_Gtid_Set,是備庫所有已經執行完成的GTID集合。

    如果這兩個集合相同,也表示備庫接收到的日志都已經同步完成。

    可見,對比位點和對比GTID這兩種方法,都要比判斷seconds_behind_master是否為0更準確。

    在執行查詢請求之前,先判斷從庫是否同步完成的方法,相比于sleep方案,準確度確實提升了不少,但還是沒有達到“精確”的程度。為什么這么說呢?

    我們現在一起來回顧下,一個事務的binlog在主備庫之間的狀態:

  • 主庫執行完成,寫入binlog,并反饋給客戶端;

  • binlog被從主庫發送給備庫,備庫收到;

  • 在備庫執行binlog完成。

  • 我們上面判斷主備無延遲的邏輯,是“備庫收到的日志都執行完成了”。但是,從binlog在主備之間狀態的分析中,不難看出還有一部分日志,處于客戶端已經收到提交確認,而備庫還沒收到日志的狀態。

    如圖4所示就是這樣的一個狀態。

    圖4 備庫還沒收到trx3

    這時,主庫上執行完成了三個事務trx1、trx2和trx3,其中:

  • trx1和trx2已經傳到從庫,并且已經執行完成了;

  • trx3在主庫執行完成,并且已經回復給客戶端,但是還沒有傳到從庫中。

  • 如果這時候你在從庫B上執行查詢請求,按照我們上面的邏輯,從庫認為已經沒有同步延遲,但還是查不到trx3的。嚴格地說,就是出現了過期讀。

    那么,這個問題有沒有辦法解決呢?

    配合semi-sync

    要解決這個問題,就要引入半同步復制,也就是semi-sync replication。

    semi-sync做了這樣的設計:

  • 事務提交的時候,主庫把binlog發給從庫;

  • 從庫收到binlog以后,發回給主庫一個ack,表示收到了;

  • 主庫收到這個ack以后,才能給客戶端返回“事務完成”的確認。

  • 也就是說,如果啟用了semi-sync,就表示所有給客戶端發送過確認的事務,都確保了備庫已經收到了這個日志。

    在第25篇文章的評論區,有同學問到:如果主庫掉電的時候,有些binlog還來不及發給從庫,會不會導致系統數據丟失?

    答案是,如果使用的是普通的異步復制模式,就可能會丟失,但semi-sync就可以解決這個問題。

    這樣,semi-sync配合前面關于位點的判斷,就能夠確定在從庫上執行的查詢請求,可以避免過期讀。

    但是,semi-sync+位點判斷的方案,只對一主一備的場景是成立的。在一主多從場景中,主庫只要等到一個從庫的ack,就開始給客戶端返回確認。這時,在從庫上執行查詢請求,就有兩種情況:

  • 如果查詢是落在這個響應了ack的從庫上,是能夠確保讀到最新數據;

  • 但如果是查詢落到其他從庫上,它們可能還沒有收到最新的日志,就會產生過期讀的問題。

  • 其實,判斷同步位點的方案還有另外一個潛在的問題,即:如果在業務更新的高峰期,主庫的位點或者GTID集合更新很快,那么上面的兩個位點等值判斷就會一直不成立,很可能出現從庫上遲遲無法響應查詢請求的情況。

    實際上,回到我們最初的業務邏輯里,當發起一個查詢請求以后,我們要得到準確的結果,其實并不需要等到“主備完全同步”。

    為什么這么說呢?我們來看一下這個時序圖。

    圖5 主備持續延遲一個事務

    圖5所示,就是等待位點方案的一個bad case。圖中備庫B下的虛線框,分別表示relaylog和binlog中的事務??梢钥吹?#xff0c;圖5中從狀態1 到狀態4,一直處于延遲一個事務的狀態。

    備庫B一直到狀態4都和主庫A存在延遲,如果用上面必須等到無延遲才能查詢的方案,select語句直到狀態4都不能被執行。

    但是,其實客戶端是在發完trx1更新后發起的select語句,我們只需要確保trx1已經執行完成就可以執行select語句了。也就是說,如果在狀態3執行查詢請求,得到的就是預期結果了。

    到這里,我們小結一下,semi-sync配合判斷主備無延遲的方案,存在兩個問題:

  • 一主多從的時候,在某些從庫執行查詢請求會存在過期讀的現象;

  • 在持續延遲的情況下,可能出現過度等待的問題。

  • 接下來,我要和你介紹的等主庫位點方案,就可以解決這兩個問題。

    等主庫位點方案

    要理解等主庫位點方案,我需要先和你介紹一條命令:

    select master_pos_wait(file, pos[, timeout]);

    這條命令的邏輯如下:

  • 它是在從庫執行的;

  • 參數file和pos指的是主庫上的文件名和位置;

  • timeout可選,設置為正整數N表示這個函數最多等待N秒。

  • 這個命令正常返回的結果是一個正整數M,表示從命令開始執行,到應用完file和pos表示的binlog位置,執行了多少事務。

    當然,除了正常返回一個正整數M外,這條命令還會返回一些其他結果,包括:

  • 如果執行期間,備庫同步線程發生異常,則返回NULL;

  • 如果等待超過N秒,就返回-1;

  • 如果剛開始執行的時候,就發現已經執行過這個位置了,則返回0。

  • 對于圖5中先執行trx1,再執行一個查詢請求的邏輯,要保證能夠查到正確的數據,我們可以使用這個邏輯:

  • trx1事務更新完成后,馬上執行show master status得到當前主庫執行到的File和Position;

  • 選定一個從庫執行查詢語句;

  • 在從庫上執行select master_pos_wait(File, Position, 1);

  • 如果返回值是>=0的正整數,則在這個從庫執行查詢語句;

  • 否則,到主庫執行查詢語句。

  • 我把上面這個流程畫出來。

    圖6 master_pos_wait方案

    這里我們假設,這條select查詢最多在從庫上等待1秒。那么,如果1秒內master_pos_wait返回一個大于等于0的整數,就確保了從庫上執行的這個查詢結果一定包含了trx1的數據。

    步驟5到主庫執行查詢語句,是這類方案常用的退化機制。因為從庫的延遲時間不可控,不能無限等待,所以如果等待超時,就應該放棄,然后到主庫去查。

    你可能會說,如果所有的從庫都延遲超過1秒了,那查詢壓力不就都跑到主庫上了嗎?確實是這樣。

    但是,按照我們設定不允許過期讀的要求,就只有兩種選擇,一種是超時放棄,一種是轉到主庫查詢。具體怎么選擇,就需要業務開發同學做好限流策略了。

    GTID方案

    如果你的數據庫開啟了GTID模式,對應的也有等待GTID的方案。

    MySQL中同樣提供了一個類似的命令:

    select wait_for_executed_gtid_set(gtid_set, 1);

    這條命令的邏輯是:

  • 等待,直到這個庫執行的事務中包含傳入的gtid_set,返回0;

  • 超時返回1。

  • 在前面等位點的方案中,我們執行完事務后,還要主動去主庫執行show master status。而MySQL 5.7.6版本開始,允許在執行完更新類事務后,把這個事務的GTID返回給客戶端,這樣等GTID的方案就可以減少一次查詢。

    這時,等GTID的執行流程就變成了:

  • trx1事務更新完成后,從返回包直接獲取這個事務的GTID,記為gtid1;

  • 選定一個從庫執行查詢語句;

  • 在從庫上執行 select wait_for_executed_gtid_set(gtid1, 1);

  • 如果返回值是0,則在這個從庫執行查詢語句;

  • 否則,到主庫執行查詢語句。

  • 跟等主庫位點的方案一樣,等待超時后是否直接到主庫查詢,需要業務開發同學來做限流考慮。

    我把這個流程圖畫出來。

    圖7 wait_for_executed_gtid_set方案

    在上面的第一步中,trx1事務更新完成后,從返回包直接獲取這個事務的GTID。問題是,怎么能夠讓MySQL在執行事務后,返回包中帶上GTID呢?

    你只需要將參數session_track_gtids設置為OWN_GTID,然后通過API接口mysql_session_track_get_first從返回包解析出GTID的值即可。

    在專欄的第一篇文章中,我介紹mysql_reset_connection的時候,評論區有同學留言問這類接口應該怎么使用。

    這里我再回答一下。其實,MySQL并沒有提供這類接口的SQL用法,是提供給程序的API(https://dev.mysql.com/doc/refman/5.7/en/c-api-functions.html)。

    比如,為了讓客戶端在事務提交后,返回的GITD能夠在客戶端顯示出來,我對MySQL客戶端代碼做了點修改,如下所示:

    圖8 顯示更新事務的GTID--代碼

    這樣,就可以看到語句執行完成,顯示出GITD的值。

    圖9 顯示更新事務的GTID--效果

    當然了,這只是一個例子。你要使用這個方案的時候,還是應該在你的客戶端代碼中調用mysql_session_track_get_first這個函數。

    小結

    在今天這篇文章中,我跟你介紹了一主多從做讀寫分離時,可能碰到過期讀的原因,以及幾種應對的方案。

    這幾種方案中,有的方案看上去是做了妥協,有的方案看上去不那么靠譜兒,但都是有實際應用場景的,你需要根據業務需求選擇。

    即使是最后等待位點和等待GTID這兩個方案,雖然看上去比較靠譜兒,但仍然存在需要權衡的情況。如果所有的從庫都延遲,那么請求就會全部落到主庫上,這時候會不會由于壓力突然增大,把主庫打掛了呢?

    其實,在實際應用中,這幾個方案是可以混合使用的。

    比如,先在客戶端對請求做分類,區分哪些請求可以接受過期讀,而哪些請求完全不能接受過期讀;然后,對于不能接受過期讀的語句,再使用等GTID或等位點的方案。

    但話說回來,過期讀在本質上是由一寫多讀導致的。在實際應用中,可能會有別的不需要等待就可以水平擴展的數據庫方案,但這往往是用犧牲寫性能換來的,也就是需要在讀性能和寫性能中取權衡。

    最后 ,我給你留下一個問題吧。

    假設你的系統采用了我們文中介紹的最后一個方案,也就是等GTID的方案,現在你要對主庫的一張大表做DDL,可能會出現什么情況呢?為了避免這種情況,你會怎么做呢?

    你可以把你的分析和方案設計寫在評論區,我會在下一篇文章跟你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。

    上期問題時間

    上期給你留的問題是,在GTID模式下,如果一個新的從庫接上主庫,但是需要的binlog已經沒了,要怎么做?

    @某、人同學給了很詳細的分析,我把他的回答略做修改貼過來。

  • 如果業務允許主從不一致的情況,那么可以在主庫上先執行show global variables like ‘gtid_purged’,得到主庫已經刪除的GTID集合,假設是gtid_purged1;然后先在從庫上執行reset master,再執行set global gtid_purged =‘gtid_purged1’;最后執行start slave,就會從主庫現存的binlog開始同步。binlog缺失的那一部分,數據在從庫上就可能會有丟失,造成主從不一致。

  • 如果需要主從數據一致的話,最好還是通過重新搭建從庫來做。

  • 如果有其他的從庫保留有全量的binlog的話,可以把新的從庫先接到這個保留了全量binlog的從庫,追上日志以后,如果有需要,再接回主庫。

  • 如果binlog有備份的情況,可以先在從庫上應用缺失的binlog,然后再執行start slave。

  • ?

  • 轉載于:https://www.cnblogs.com/gaosf/p/11149734.html

    總結

    以上是生活随笔為你收集整理的28 | 读写分离有哪些坑?的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。