SQLServer 數據異構實時同步之數據時序的問題

一、數據異構實時同步簡介

數據異構實時同步是指將數據從源端數據庫近實時的同步至目的端數據庫的一個過程,比如將 SQLServer 中的數據同步至 HBase 或 Kafka 中。不同於離線同步,實時同步需要解決變更數據採集與數據時序等問題,以此保證數據的一致性。本文主要介紹以 SQLServer 爲源端進行數據同步的過程中產生的數據時序相關的問題。
在這裏插入圖片描述
不同於 MySQL 可以通過解析 BinLog 的方式採集變更數據,在 SQLServer 中,由於其本身的特徵,主要使用以下方式採集變更數據:

  • Trigger:使用觸發器將變更數據記錄在一張單獨的表中,通常僅記錄源表主鍵及操作類型;
  • CT:開啓 CT,以記錄數據的變更,包含事務信息及源表主鍵;
  • CDC:開啓 CDC,以記錄數據的變更,包含全量數據;

CDC 由於需要太多的資源,所以僅考慮 Trigger 以及 CT 的方式,其大致流程爲:通過 Trigger 或 CT 的方式獲取到變更數據的主鍵,然後根據主鍵再去源表拿到源數據即可實現變更數據的獲取。
在這裏插入圖片描述

二、數據時序的問題

在獲取到變更數據後,數據需要寫入目的端數據庫並且同時推進遊標(比如記錄最後完成的主鍵或版本號等)纔算完成了整個同步流程。

推進遊標是爲了程序在重啓或發生故障後,能繼續上一次結束的位置繼續同步數據。如果僅僅是使用單線程,並且僅寫入一個目的端且寫入數據與遊標推進是同步操作的情況,那麼便不太可能會出現數據時序的問題。但現實中,我們很可能需要將一份源端的數據同步至多個目的端,並且爲了提高吞吐量並減少數據的延遲,需要使用多線程以及利用流的特點來完成數據同步。此時便可能發生數據時序的問題。

數據時序的問題是指數據的操作能否按照在源端操作的順序有序同步到目的端。比如針對 x=1 的數據,在源端進行了 x=2 以及 x=3 的兩次操作,當它們被同步到目的端的時候,必須保證 x=2 的這次操作優先於 x=3 操作執行,才能保證目前端的數據最終爲 x=3。如果 x=3 優先於 x=2 執行,那麼目的端的數據將最終爲 x=2,此時系統中的數據將處於不一致的狀態。所以解決數據時序的問題的主要目的是要保證數據的一致性。

如果我們能在數據寫入即使亂序也仍然能保證數據的一致性,那麼數據時序的問題便不是問題。比如 HBase 可以利用 timestamp 來實現數據的冪等性更新。但是仍然會有系統需要保證數據的操作順序是一致的,基於此,可將數據的順序分爲以下兩種:

  1. 數據全局有序;
  2. 數據以主鍵爲單位的有序;

數據全局有序是指數據的所有操作都按照先後順序依次同步至目的端,這通常只能使用單線程完成,效率低下且應用場景較少。實際場景中,僅需要以 key(行)爲單位,保證其有序,即可滿足大部分應用場景,所以這裏僅討論數據以 key 爲單位的有序的情況。

三、數據以主鍵爲單位的有序

對於以下數據及其操作,我們僅需要保證每個主鍵對應的操作是有序的即可,即只要 { OP_1, OP_3, OP_5 } 與 { OP_2, OP_4 } 內部的先後順序是一致的,即使 OP_2 優先與 OP_1 執行也沒關係。

id 操作
1 OP_1, OP_3, OP_5
2 OP_2, OP_4

在確定此前提後,利用流式數據傳輸以及多線程 hash 分發數據,並結合 Kafka 的分片(partition)模型,便可實現一個高吞吐多併發的數據同步模型。
在這裏插入圖片描述
確定數據以主鍵爲單位的有序性之後,還得面對一個問題,即數據操作被重複發送的問題。

四、數據操作被重複發送

在引入 kafka 後,數據先寫入 kafka 再通過訂閱端寫入目的端。kafka 與訂閱端會保證數據一定會寫入目的端,所以遊標的推進僅需要在數據寫入至 kafka 之後便可更新。

數據被 hash 至不同的子線程後,每個子線程僅需保證該線程的數據寫入順序即可,並可通過同步寫入數據與遊標推進操作來保證每行數據的操作至多將最新的操作重複發送一次,如果每次重啓僅僅是將最新的操作重發一次,那麼在訂閱端執行兩次相同操作不會產生什麼影響。但是在實際的項目中,我發現同步寫入數據與遊標推進操作會影響系統的同步效率,因爲遊標推進需要更新其它存儲系統中的遊標值。如果更新較爲頻繁即使系統交互之間的延遲即使不大,也會影響到同步效率。相同線程的情況下,速度慢3 ~ 4倍以上也是可能的。

所以如果每行數據即使被同時重複發送多次操作也不會影響數據的一致性,那麼便可以將數據寫入與遊標推進更改爲異步,通過異步 ack 機制來完成遊標推進。

五、數據操作的重複發送與影響

假設有以下數據:

id x
1 0

對 id=1 的數據有如下操作:

操作名 時間 更新 結果 x
OP_1 t1 x = 1 1
OP_2 t2 x = 2 2
OP_3 t3 x = 3 3

正常的情況下,數據同步的過程中會依次同步 OP_1、OP_2、OP_3 這三次操作,並依次(或合併)更新遊標,這時反映在目的端的數據 x 的值也會依次被更新,並最終爲 x=3。

操作名 時間 源數據
OP_1 t1 id = 1, x = 1
OP_2 t2 id = 1, x = 2
OP_3 t3 id = 1, x = 3

但是如果數據在同步至目的端之後,遊標未推進的情況下,系統發生異常被重啓了,此時則會重新發送 OP_1、OP_2、OP_3 這三次操作。僅憑想象的話,也許我們會認爲目的端的數據會被重新按照 1、2、3 的順序被賦值,從而導致數據出現短暫的回退現象,運氣再差點的話,OP_3 操作還未來得及執行,就被異常停止了,此時 x 的值將退回到 x=2,數據發生了錯誤。

但是實際上,由於使用 trigger 或 CT 的方式記錄表的變更,它們僅包含源表的主鍵(或必要的信息),在獲取到變更操作之後,需要再次到源表獲取源數據,所以即使系統被重啓,這三次操作所攜帶的源數據也應該是相同的:

操作名 時間 源數據
OP_1 t4 id = 1, x = 3
OP_2 t4 id = 1, x = 3
OP_3 t4 id = 1, x = 3

此時反映到目的端的操作可能如下:

操作名 時間 源數據
OP_1 t1 id = 1, x = 1
OP_2 t2 id = 1, x = 2
OP_3 t3 id = 1, x = 3
OP_1 t4 id = 1, x = 3
OP_2 t4 id = 1, x = 3
OP_3 t4 id = 1, x = 3

可以發現對目的端數據無影響。同樣的,引入刪除操作也將會是同樣的結論,正常情況下,每次操作攜帶的數據如下:

操作名 時間 源數據
OP_1 t1 id = 1, x = 1
OP_2 t2 id = 1, x = 2
OP_3 t3 id = 1, x = 3
OP_4 (DELETE) t4 null

發生異常重啓後,因爲源數據已經被刪除,所以它們將都會空:

操作名 時間 源數據
OP_1 t5 null
OP_2 t5 null
OP_3 t5 null
OP_4 (DELETE) t5 null

前提是:主鍵被刪除後不可再恢復!

六、結論

數據異構同步的一個難點就在於保證數據的時序性,諸如 HBase 的多版本特徵可以自主保證數據的一致性,其它如 Redis、Solr 等則可能需要由同步器保證數據同步寫入的順序。而得知在一些場景下,數據操作可以在系統被重啓後被重複發送,有助於改善遊標推進的實現,因爲同步更新遊標極可能會影響到數據同步的性能,而遊標的主要目的是用於保證系統在異常或正常重啓後能夠繼續上一次同步的位置繼續同步,但是對於一個實時在線同步的系統而言,被重啓的情況本就是很少的。

注:以上分析僅針對以 SQLSever 爲源端的數據同步的特定場景,不能概括所有場景。

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