前段時間重做了一個數據同步方面的工作,具體是把相關數據從一個遠端複製到本地端,需要做到不重、不漏、無誤的同時保持使用簡單方便。
背景:
我們跟第三方合作,可以讀取使用第三方的數據源,但不能修改,而第三方的數據源沒有我們想要的索引和主鍵約束,所以想要更方便地使用第三方數據必須把數據同步傳輸到本地,根據自己的需求加索引加主鍵約束等等。第三方數據庫中的數據會增加,修改,但不被會刪除。
原來的方案:
根據數據源的特點,把我們需要的字段同步過來,根據數據特性加上主鍵約束來確保數據的唯一性,更新操作通過主鍵來做對應。
每次同步都只取前一天更新的源數據進行更新操作。
遇到的問題:
- 經過不斷地踩坑後發現,原來第三方的數據源不止是會增加,還會進行修改,不止是普通字段的修改,我們自建的主鍵中的值也會修改,雖然發生的頻率很小,由於數據特性,不會發生主鍵衝突,但主鍵都改了,兩份數據就沒法對應了,這就導致了會多出一些“錯誤”數據。
- 利用cron做的定時任務,一旦某次數據衝突或者發生硬件故障沒有更新,那麼在解決了故障之後必須手動的傳入參數把前N天的數據都做一遍更新。
- 原來的方案數據是一條一條的查找的,效率十分低下。
所以很有必要重新做一個同步方案。
新的嘗試
經過觀察,第三方數據庫中每一張表都有且僅有一個唯一主鍵的id,此外還有數據更新時間op_date。所以我們只能把唯一主鍵也同步過來。
方案1
- 借鑑以前的老方案,根據傳入的參數來取相應的數據。傳入的參數中有一個參數指定同步N天以前直到當前最新一天期間發生變更的數據。源數據是一次性取出來的,本地數據庫中的數據也是一次性取出來,然後做對應。
這種方案,數據正確性可以保證,但是其他方面還是不盡如人意。而且,假如說某一個天的數據量特別巨大的話,會不會發生內存不夠?既然選擇重做,那就做好,把一切有可能發生的問題都避免掉。所以可以由一次性同步改爲分批次同步。這是一個小的優化點。
其實也還有其他的問題,假如更新了一般發生了意外停止了更新而又沒有被監控到怎麼辦?
方案2
這個時候老大看了我的設計方案,我們思索討論了一下,有了一個新的思路。
這裏引入了兩個新名詞:全量更新和差量更新。
全量更新是從零開始全部更新,差量更新是從上次更新的地方接着更新。差量更新的好處在於,及時上次因爲某種意外斷掉了,也不會影響下一次更新,並且下一次更新會把上一次未更新的數據也更新了。
最後成型的具體方案是:
- 獲取local庫相應表中最新的數據,以op_date和id倒序排列。記最新數據的op_date和id分別爲l_op_date, l_id。
- 獲取origin庫相應表中,op_date 等於 l_op_date的數據進行同步,一次最多取max_count個,直到取完。
- 獲取origin庫響應表中,op_date 大於 l_op_date、id 大於 l_id的數據,以op_date和id倒序排列,最多取max_count個,同步更新並記 l_op_date、l_id爲最新條數據相應的op_date和l_id。
- 重複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行就可以完成一個新的傳輸類。具體代碼就不展示了。