使用canal同步MySQL數據到ES的有序性保證

最近在做的項目中有用到canal實時同步MySQL的數據,並且寫入es的場景,總結了一些心得,以備後查。
總體同步的流程圖如下:

 

MySQL-es process.png

 

鏈路中的環節稍微解釋下:

  • binlog MySQL的自身的操作日誌,用來記錄數據的變更操作及變更後的數據。需要開啓並配置 binlog-format 爲 ROW 模式。具體可查看canal文檔

  • canal alibaba開源的用來同步MySQL binlog的工具,簡單來說canal就是把自己僞裝成了MySQL的一個slave,然後同步其binlog。具體使用方法和原理可以查看github主頁https://github.com/alibaba/canal,裏面寫的很清楚了。P.S.這裏表揚一下阿里巴巴,最近幾年擁抱開源社區,開源了好多好用的工具,如datax,canal,druid等。

  • rocketMQ 阿里巴巴出品的消息中間件,現已孵化爲apache頂級項目。具體文檔可參考官網http://rocketmq.apache.org/

  • application自己實現的一個應用,主要的作用是消費mq中的消息,並且將其寫入es。

  • Elasticsearch 基於Lucene的一款全文搜索引擎,在MySQL無法處理的海量數據的查詢場景中發揮着重要的作用。具體文檔可參考官網https://www.elastic.co/

爲什麼要保證有序性?原因很簡單:更新和刪除的操作,如果出現了先發後至/後發先至的情況,就會導致es裏的數據是比較舊的數據。

對於如何保證有序性,我們首先要分析一下到底是哪一步破壞了有序性。一步步從頭看:

  1. MySQL => binlog,顯然它一定是有序的;
  2. binlog => canal,canal的原理是僞裝成slave來dump binlog,這也是有序的,否則MySQL主從就不同步了;
  3. canal => rocketMQ,通過查閱canal的官方文檔中的mq順序性問題一節,發現只有指定了pkhash消息投遞方式,且pk出現變更纔可能出現無序,由於我們並不會出現這種情況,所以這步也是有序的;
  4. rocketMQ => application, 通過查閱rocketMQ的官方文檔中的consumer一節,發現消息的消費是否有序取決與使用的是OrderlyListener還是ConcurrentlyListener,顧名思義,前者是保證有序消費的,後者是不保證但吞吐量更高的,所以這裏是一個可以設置的點;
  5. application => es, 通過查閱Elasticsearch的官方文檔中的versioning
    一節,發現es是通過版本號來控制更新/刪除操作的有序性的,版本控制又分爲兩種:internalexternal,內部版本號是默認使用的,即操作時傳入的版本號必須與文檔當前版本號一致纔可以操作成功,而外部版本號則是操作時傳入的版本號必須大於文檔當前版本號纔可以操作成功,這裏又是一個可以設置的點。

從上面的分析可得,我們可以在兩個環節進行有序性的保證,一種是在第4部消費消息的時候保證有序性,另一種是在第5步es寫入的時候保證有序性(更準確地說,應該是保證更新的數據總是能覆蓋舊的數據)。

第一種實現方式很簡單,只要在你的application中使用OrderlyListener來消費rocketMQ的消息,並且在寫入es異常時返回ConsumeConcurrentlyStatus.RECONSUME_LATER,就能保證消息消費的有序性及消息消費成功(寫入es)纔會消費下一條消息。但由於這裏用的是OrderlyListener,犧牲了併發的性能。而實際場景中我們關心其實是對於同一條數據的操作的有序性,而非整張表操作的有序性,在一個表一個topic的配置下所有的消息都變得串行了。所以爲了達到更高吞吐量可以考慮下面一種方式。

第二種實現方式也是我比較推崇的,這需要表結構的配合,並在操作es時使用external外部版本號。首先需要爲每條操作設置一個版本號,且新的操作的版本號一定要大於舊的操作,很顯然,數據的變更時間戳很適合作這個版本號。那麼我們就需要在創建表的時候爲表增加一個updated_time並且由數據庫來維護:

 

updated_time datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)

其實這個應該是每個表都默認有的字段,我在《設計之道-數據庫設計》也有提過。這裏和當時不同的是將精度設置爲了3(毫秒),datetime不指定的話默認是秒。因爲在實際業務場景中,同一秒鐘同一條數據發生多次更新是很有可能的,如果設置成秒的話,生成的版本號就相同了,那麼就無法正確寫入es了。當然如果你的業務夠牛逼,同一毫秒都能有多次更新,那麼你可以設置爲6(微秒)。這樣我們在消費消息的時候就可以使用性能更好的ConcurrentlyListener併發地消費消息,只要在es寫入時指定使用數據的updated_time時間戳作爲外部版本號,這樣即使出現了先發後至的情況,較老的數據由於版本號一定比新的數據更小,也無法覆蓋es中已經存在的數據。

這裏可以稍微展示下ES中指定外部版本號後更新的現象:

  1. 創建一個索引,這時我們能看到它的版本號是1。

     

    創建索引

  2. 將這個索引做一次全量替換,我們發現它的版本號變爲了2。

     

    更新索引

  3. 接下來我們繼續更新索引,這時我們指定外部版本號,並將版本號設置爲1,便會出現version confict,原因也很明確:當前版本號2大於或等於傳入的版本號1,寫入失敗。

    使用比較舊的版本號更新

     

  4. 接下來我們使用版本號3進行更新,看到es返回更新成功,並且版本號也變爲了我們指定的3。

     

    使用新的版本號進行更新

上面的步驟3、4,確保了ES中即使出現了先發後至/後發先至的情況,在使用數據庫update_time作爲版本號時也不會出現老數據覆蓋新數據的情況。



作者:SawyerZhou
鏈接:https://www.jianshu.com/p/2144250176d9
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章