您好,登錄后才能下訂單哦!
導語
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 發送消息,通過異步消息驅動后續業務流程,以實現消息驅動的設計。大致的業務流程示意圖如下:
圖1:直接發送消息的訂單事件驅動
圖2:基于 Binlog 的訂單事件驅動
暗 藏 問 題
上文提到,雖然基于 Binlog 的訂單事件驅動設計存在諸多優點,但后來發現其實暗藏問題。經過實驗,我們發現偶爾會有訂單履約延遲的現象。
在正常流程中,訂單履約服務收到訂單支付事件后,會檢查訂單狀態,如果此時訂單狀態為已支付,則進行履約流程的處理。但對于有履約延遲的訂單,訂單履約服務收到此訂單的支付事件后,查詢數據庫發現此訂單并非支付狀態。經過調查,我們排除了數據并發覆蓋問題,并且訂單狀態查詢是發生在主庫上,也不存在主從同步延遲問題。
那究竟是什么原因導致業務系統收到根據 Binlog 生成的訂單支付事件后,再查詢主庫得到的訂單數據卻是未支付狀態的?
對于此問題的原因我們先放下不談,先來看看 MySQL 在更新數據時的內部原理。
Redo Log | Binlog | |
日志類型 | 物理日志,即數據頁中的真實二級制數據,恢復速度快 | 邏輯日志,SQL 語句 (statement) 或數據邏輯變化 (row),恢復速度慢 |
存儲格式 | 基于 InnoDB 數據頁格式進行存儲 | SQL 語句或數據變化內容 |
用途 | 重做數據頁 | 數據復制 |
層級 | InnoDB 存儲引擎層 | MySQL Server 層 |
記錄方式 | 循環寫 | 追加寫 |
圖中描述了 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(郵件標題請注明:會員開發)
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。