亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

MySQL Binlog 技術原理和業務應用案例分析

發布時間:2020-08-06 23:38:05 來源:ITPUB博客 閱讀:182 作者:愛奇藝技術產品團隊 欄目:MySQL數據庫

導語


MySQL Binlog用于記錄用戶對數據庫操作的結構化查詢語言(Structured Query Language,SQL)語句信息。是MySQL數據庫的二進制日志,可以使用mysqlbin命令查看二進制日志的內容。愛奇藝在會員訂單系統使用到了 MySQL Binlog,用來實現訂單事件驅動。在使用Binlog 后在簡化系統設計的同時幫助系統提升了可用性和數據一致性。

本文將從實際應用角度出發理解 MySQL中的相關技術原理,從技術原理和工作實踐相結合,幫助大家以及在相關設計中存在的潛在問題,希望能給大家有所幫助和啟發,共同進步。

作者介紹:作者帆叔目前主要負責愛奇藝會員交易系統的技術和架構工作,專注異步編程、服務治理、代碼重構等領域,熱愛技術,樂于分享。

背 景

Binlog 是 MySQL 中一個很重要的日志,主要用于 MySQL 主從間的數據同步復制。正是因為 Binlog 的這項功用,它也被用于 MySQL 向其它類型數據庫同步數據,以及業務流程的事件驅動設計。通過研究分析,我們發現使用 MySQL Binlog 實現事件驅動設計并沒有想象中那么簡單,所以接下來帶大家了解 MySQL 的 Binlog、Redo Log、數據更新內部流程,并通過對這些技術原理的介紹,來分析對業務流程可能造成的問題,以及如何避免這些問題。希望通過本文的解析,能夠幫助大家了解到 MySQL 的一些原理,從而幫助大家能夠更順利地使用 MySQL 這個流行的數據庫技術。

基于 Binlog 的事件驅動

首先介紹一下會員訂單系統的設計,訂單系統直接向 MQ 發送消息,通過異步消息驅動后續業務流程,以實現消息驅動的設計。大致的業務流程示意圖如下:

MySQL Binlog 技術原理和業務應用案例分析

 圖1:直接發送消息的訂單事件驅動 

這種設計需要保證數據庫操作和消息操作的數據一致性,即數據保存和消息發送要全部成功或者全部失敗。顯然在數據保存前和事務中進行消息發送都是不合適的。我們是在數據更新操作后,數據庫事務外發送消息。如果數據保存成功,但消息發送失敗,支付系統需要重新通知(上圖步驟1),直至通知成功。這種設計雖然實現了功能和對可用性的基本要求,但存在如下缺點:
1. 業務系統直接依賴消息中間件
消息中間件的故障,不僅會影響支付通知的處理也可能影響業務系統上的其它接口。
2. 業務系統必須實現可靠的重試
不論是請求發起方還是請求接收方都必須實現可靠重試才能實現最大努力通知的目標。
3. 重試間隔增大會造成業務延遲
隨著重試次數增加,每次重試的間隔通常也越來越大,這成為 Exponential Backoff(指數級退避)。這種設計能夠讓請求接收方的故障處理更加從容,避免因密集重試造成請求接收方服務難以恢復。但這樣做可能會使請求接收方在恢復服務之后很長時間后才處理完積壓的消息,從而造成業務延遲。我們可以采用類似 Hystrix 的自適應設計,在請求接收方服務恢復后回到到正常的請求速率。但這樣的設計顯然會復雜許多。
為了解決上述問題,簡化技術架構,我們采用事件表的設計思想,將訂單表作為事件表。通過訂閱訂單表的 Binlog,生成訂單事件,驅動后續業務流程。在系統架構上,業務系統不用直接依賴消息中間件,只需專注數據庫操作。而通過引入一個接收 Binlog 的獨立的系統,將 MySQL 數據變化轉換成業務事件驅動后續流程。具體流程如下:

MySQL Binlog 技術原理和業務應用案例分析

圖2:基于 Binlog 的訂單事件驅動 

暗 藏 問 題

上文提到,雖然基于 Binlog 的訂單事件驅動設計存在諸多優點,但后來發現其實暗藏問題。經過實驗,我們發現偶爾會有訂單履約延遲的現象。

在正常流程中,訂單履約服務收到訂單支付事件后,會檢查訂單狀態,如果此時訂單狀態為已支付,則進行履約流程的處理。但對于有履約延遲的訂單,訂單履約服務收到此訂單的支付事件后,查詢數據庫發現此訂單并非支付狀態。經過調查,我們排除了數據并發覆蓋問題,并且訂單狀態查詢是發生在主庫上,也不存在主從同步延遲問題。

那究竟是什么原因導致業務系統收到根據 Binlog 生成的訂單支付事件后,再查詢主庫得到的訂單數據卻是未支付狀態的?

對于此問題的原因我們先放下不談,先來看看 MySQL 在更新數據時的內部原理。

MySQL 數據更新相關原理

本節將向大家介紹 MySQL 數據更新相關原理,以及在這一過程中最重要的兩種日志:Redo Log 和 Binlog。
>>>> Redo Log 和 Binlog
先來介紹 Redo Log 和 Binary Log(Binlog):
· Redo LogRedo Log 是 InnoDB 存儲引擎提供的一種物理日志結構,用來描述對底層數據頁操作的具體內容,主要用于實現 crash-safe,并提升磁盤操作效率。
· BinlogBinlog 是 MySQL 本身提供的一種邏輯日志,和具體存儲引擎無關,描述的是數據庫所執行的 SQL 語句或數據變更情況,主要用于數據復制。
InnoDB 引入 Redo Log 的目的在于實現 crash-safe 和提升數據更新效率。如果 InnoDB 每次數據寫操作都要直接持久化到磁盤上的數據頁中,那樣會大量增加磁盤隨機 IO 次數。引入 Redo Log 后,在對數據寫操作時,會將部分隨機 IO 寫變為順序寫。因為磁盤的順序 IO 效率遠高于隨機 IO,因此引入 Redo Log 機制有助于提升更新數據時的性能(如何實現 crash-safe 將在下一節介紹)。
下面的表格說明了兩種日志的作用和它們的不同:

Redo Log

Binlog

日志類型

物理日志,即數據頁中的真實二級制數據,恢復速度快

邏輯日志,SQL 語句 (statement) 或數據邏輯變化 (row),恢復速度慢

存儲格式

基于 InnoDB 數據頁格式進行存儲

SQL 語句或數據變化內容

用途

重做數據頁

數據復制

層級

InnoDB 存儲引擎層

MySQL Server 層

記錄方式

循環寫

追加寫

這時問題來了,現在 MySQL 中存在了兩種日志結構:Redo Log 和 Binlog。雖然它們的結構和功能有所不同,但卻記錄著相同的數據。如何保證這兩種日志數據的一致性,以及如何實現 crash-safe 呢?這就引出了兩階段提交設計。

>>>> 兩階段提交

兩階段提交不是 Redo Log 或 InnoDB 中的設計,而是 MySQL 服務器的設計(但通常說到兩階段提交時都和 Redo Log 放在一起)。因為 MySQL 采用插件化的存儲引擎設計,事務提交時,服務器本身和存儲引擎都需要提交數據。所以從 MySQL 服務器角度看,其本身就面臨著分布式事務問題。
為解決此問題,MySQL 引入了兩階段提交。在兩階段提交過程中,Redo Log 會有兩次操作:Prepare 和 Commit。而 Binlog 寫操作則夾在 Redo Log 的 Prepare 和 Commit 操作之間。我們可以設想一下不同失敗場景下兩階段提交的設計是如何保證數據一致的:
1. Redo Log Prepare 成功,在寫 Binlog 前崩潰:在故障恢復后事務就會回滾。這樣 Redo Log 和 Binlog 的內容還是一致的。這種情況比較簡單,比較復雜的是下一種情況,即在寫 Binlog 和 Redo Log Commit 中間崩潰時,MySQL 是如何處理的?
2. 在寫 Binlog 之后,但 Redo Log 還沒有 Commit 之前崩潰
  • 如果 Redo Log 有 Commit 標識,說明 Redo Log 其實已經 Commit 成功。這時直接提交事務;
  • 如果 Redo Log 沒有 Commit 標識,則使用 XID(事務 ID)查詢 Binlog 相應日志,并檢查日志的完整。如果 Binlog 是完整的,則提交事務,否則回滾;
如何判斷 Binlog 是否完整?簡單來說 Statement 格式的 Binlog 最后有 Commit,或 Row 格式的 Binlog 有 XID Event,那 Binlog 就是完整的。

>>>> MySQL 數據更新流程

接下來看一下 MySQL 執行器和 InnoDB 存儲引擎在執行簡單 update 語句 update t set n = n + 1 where id = 2 時的流程(因為此例只執行單條更新語句,所以其自身就是一個事務)。
  1. 執行器先找引擎取 ID=2 這一行。ID 是主鍵,引擎直接用樹搜索找到這一行。如果 ID=2 這一行所在的數據頁本來就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,然后再返回。
  2. 執行器拿到引擎給的行數據,把這個值加上1,比如原來是 N,現在就是 N+1,得到新的一行數據,再調用引擎接口寫入這行新數據。
  3. 引擎將這行新數據更新到內存中。然后將對內存數據頁的更新內容記錄在 Redo Log Buffer 中(這里不詳細介紹 Redo Log Buffer。只需知道對 Redo Log 的操作并不會直接寫在文件上,而是先記錄在內存中,然后在特定時刻才會寫入磁盤)。此時完成了數據更新操作。
  4. 接下來要進行事務提交的操作。事務提交時,Redo Log 被標記為 Prepare 狀態。通常此時,Redo Log 會從 Buffer 寫入磁盤(innodb_flush_log_at_trx_commit,值為1時,每次提交事務 Redo Log 都會寫入磁盤)。然后 InnoDB 告知執行器執行完成,可以提交事務。
  5. 執行器生成本次操作的 Binlog,并把 Binlog 寫入磁盤。
  6. 執行器調用引擎的提交事務接口,引擎把剛剛寫入的 Redo Log 改成提交 Commit 狀態,更新完成。

MySQL Binlog 技術原理和業務應用案例分析

圖中描述了 update 語句執行過程中 MySQL 執行器、InnoDB,以及 Binlog、Redo Log 交互過程(圖中深綠底色的是 MySQL 執行器負責的階段,淺綠底色是 InnoDB 負責的階段)

問題解析

從上面對 MySQL 原理的介紹我們得知,寫 Binlog 發生在事務提交階段,但是 MySQL 因為在 Server 層和存儲引擎層都引入了不同的日志結構,從而引入了兩階段提交。Binlog 的寫入發生在存儲引擎真正提交事務之前,這導致理論上通過 Binlog 同步數據的系統(MySQL 從庫、其它數據庫或業務系統)有可能早于 MySQL 主庫使最新提交的數據生效。

所以上面提到的訂單履約服務在收到基于 Binlog 的訂單支付事件后卻查到相應訂單是未支付的,原因很可能是訂單履約服務在查詢數據時,訂單支付數據更新操作在 MySQL 內部尚未徹底完成事務的提交。

我們通過開發驗證程序重現了這一現象。驗證程序接收到事務提交完成后的完整 Binlog 時會再次在 MySQL 主庫上查詢對應的記錄,結果會有一定概覽獲得事務提交前的數據。

另外經過了解,也有同行反映遇到過從庫早于主庫看到數據提交的問題。

問題的解決方法

在了解問題背后的原因之后,我們需要思考如何解決此問題。目前解決此問題有兩個方法:重試和直接使用 Binlog 數據。

重試這種做法簡單粗暴,既然問題原因是 Binlog 早于事務提交,那等一下再重試查詢自然就解決了。但在實踐中,需要考慮重試的實現方法、以及是否會因為重試過多甚至無限重試導致服務異常。對于重試的實現,可使用的方法有線程 Sleep 大法和消息重投等方式。線程 Sleep 大法通常是不被推薦的,因為它會導致線程利用率降低,甚至導致服務無法響應。但考慮到本次問題出現概率較低,我們認為線程 Sleep 大法是可以使用的,并且此方式簡單易行,可用于問題的快速修復。

第二種重試方式是消息重投,比如 RocketMQ 中 Consumer 返回 ConsumeConcurrentlyStatus.RECONSUME_LATER 即可觸發消息重投。但這種重試方法成本較前一種方法高,另外重試間隔也相對較大,對時間敏感的業務影響也較大,因此是否采用此方法需從業務和技術兩個角度綜合考慮。

除了考慮用何種方式重試,還要考慮 ABA 問題,即狀態變化按照 A->B->A 的方式進行。業務系統期待的狀態是 B,但實際可能沒辦法再變成 B 了。因此在用重試解決此問題之前,需要先排除業務系統存在 ABA 問題的可能。對于狀態 ABA 問題,可用狀態機等方式解決,這里不再展開討論。

除了重試,另一種方法就是直接使用 Binlog。因為 Binlog (row 格式) 直接反映了數據的變化情況,其中可以記錄事務提交涉及到的完整數據,因此可直接用作業務處理。這樣還可以降低數據庫 QPS。如果是新設計的系統,我認為這樣做法比較理想。但對于已有系統,這種方式改動可能較大,是否采用需權衡成本和收益。

招聘信息

愛奇藝會員開發團隊誠招 Java 資深工程師/技術專家。會員業務是愛奇藝核心業務之一,我們致力于通過技術手段服務核心業務,研發通用化、高可用的業務系統,同時我們也需要擅長如數據庫、服務治理、MQ 等技術的人才。歡迎感興趣的同學發送簡歷至:luodi@qiyi.com(郵件標題請注明:會員開發)

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

北辰区| 江津市| 鹤岗市| 云龙县| 武强县| 隆林| 富民县| 军事| 丹巴县| 南阳市| 万载县| 盐城市| 克什克腾旗| 壶关县| 襄垣县| 五莲县| 馆陶县| 仪陇县| 肥西县| 图木舒克市| 博客| 武隆县| 响水县| 蒲城县| 皮山县| 龙口市| 孝感市| 普宁市| 原阳县| 陇南市| 连山| 桐庐县| 临沂市| 罗平县| 祁连县| 武安市| 万安县| 宁晋县| 罗江县| 滕州市| 花莲县|