論一數據同步方案

前段時間重做了一個數據同步方面的工作,具體是把相關數據從一個遠端複製到本地端,需要做到不重、不漏、無誤的同時保持使用簡單方便。

背景:

我們跟第三方合作,可以讀取使用第三方的數據源,但不能修改,而第三方的數據源沒有我們想要的索引和主鍵約束,所以想要更方便地使用第三方數據必須把數據同步傳輸到本地,根據自己的需求加索引加主鍵約束等等。第三方數據庫中的數據會增加,修改,但不被會刪除。

原來的方案:

根據數據源的特點,把我們需要的字段同步過來,根據數據特性加上主鍵約束來確保數據的唯一性,更新操作通過主鍵來做對應。
每次同步都只取前一天更新的源數據進行更新操作。

遇到的問題:

  1. 經過不斷地踩坑後發現,原來第三方的數據源不止是會增加,還會進行修改,不止是普通字段的修改,我們自建的主鍵中的值也會修改,雖然發生的頻率很小,由於數據特性,不會發生主鍵衝突,但主鍵都改了,兩份數據就沒法對應了,這就導致了會多出一些“錯誤”數據。
  2. 利用cron做的定時任務,一旦某次數據衝突或者發生硬件故障沒有更新,那麼在解決了故障之後必須手動的傳入參數把前N天的數據都做一遍更新。
  3. 原來的方案數據是一條一條的查找的,效率十分低下。

所以很有必要重新做一個同步方案。

新的嘗試

經過觀察,第三方數據庫中每一張表都有且僅有一個唯一主鍵的id,此外還有數據更新時間op_date。所以我們只能把唯一主鍵也同步過來。

方案1

  • 借鑑以前的老方案,根據傳入的參數來取相應的數據。傳入的參數中有一個參數指定同步N天以前直到當前最新一天期間發生變更的數據。源數據是一次性取出來的,本地數據庫中的數據也是一次性取出來,然後做對應。

這種方案,數據正確性可以保證,但是其他方面還是不盡如人意。而且,假如說某一個天的數據量特別巨大的話,會不會發生內存不夠?既然選擇重做,那就做好,把一切有可能發生的問題都避免掉。所以可以由一次性同步改爲分批次同步。這是一個小的優化點。

其實也還有其他的問題,假如更新了一般發生了意外停止了更新而又沒有被監控到怎麼辦?

方案2

這個時候老大看了我的設計方案,我們思索討論了一下,有了一個新的思路。
這裏引入了兩個新名詞:全量更新和差量更新。
全量更新是從零開始全部更新,差量更新是從上次更新的地方接着更新。差量更新的好處在於,及時上次因爲某種意外斷掉了,也不會影響下一次更新,並且下一次更新會把上一次未更新的數據也更新了。

最後成型的具體方案是:

  1. 獲取local庫相應表中最新的數據,以op_date和id倒序排列。記最新數據的op_date和id分別爲l_op_date, l_id。
  2. 獲取origin庫相應表中,op_date 等於 l_op_date的數據進行同步,一次最多取max_count個,直到取完。
  3. 獲取origin庫響應表中,op_date 大於 l_op_date、id 大於 l_id的數據,以op_date和id倒序排列,最多取max_count個,同步更新並記 l_op_date、l_id爲最新條數據相應的op_date和l_id。
  4. 重複2-3,直到取完所有數據。

這樣的話,每次更新操作都會接着上一次的操作繼續執行,並且無論多大的數據量都不會發生內存不夠的情況,無論上次同步更新到哪兒了,下一次更新的時候總會接着上一次的更新位置繼續執行。

代碼大致如下

class BaseTransfer(object):
    max_count = 2000

    def run():
        count = 0
        exec_count = 0

        for origin_data in self.get_origin_data():
            local_data_dict = {x.id: x for x in self.get_local_data(origin_data)}

            for item in origin_data:
                if item.is_broken():
                    continue

                # 特殊的檢測
                if not self.special_check(item):
                    continue

                mn = mass_dict.get(item.id)
                if mn:
                    # 更新
                    if self.update(item, mn):
                        count += 1
                else:
                    # 插入
                    self.add(item)
                    count += 1

                if count - exec_count >= self.max_count:
                    exec_count = count
                    db.session.commit()

        db.session.commit()

    def get_origin_data(self):
        # 獲取op_date和wind_id
        local_data = self.get_lastest_record()

        # 第一次導入全部數據的時候需要考慮沒有數據的情況
        if not local_data:
            l_op_date = datetime(1980, 1, 1)
            l_id = ''
        else:
            l_op_date = local_data.op_date
            l_id = local_data.id

        while True:
            # 先做op_date等量查詢
            while True:
                origin_data = 等量查詢
                if not origin_data:
                    break

                l_id = origin_data[-1].id
                yield origin_data

            # 大於當前op_date
            origin_data = origin_model.query.filter(
                origin_model.op_date > l_op_date
            ).order_by(
                origin_model.op_date,
                origin_model.id_
            ).limit(self.max_count).all()

            if not origin_data:
                break

            l_op_date = origin_data[-1].op_date
            l_id = origin_data[-1].id
            yield origin_data

            if len(origin_data) < self.max_count:
                break

當然,其中做了很多的優化,比如說可重用性,擴展性等等。想要做多個表的同步更新,每個表傳輸類只需要繼承自BaseTransfer,然後實現自己獨有的一些特殊方法就可以了,只需區區5,6行就可以完成一個新的傳輸類。具體代碼就不展示了。

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