根據時間戳,增量同步資料的解決辦法
由於markdown的樣式太醜了,懶得再調整了,我另外再貼一個github的部落格 ofollow,noindex" target="_blank">該文的 github連結
前言
最近在工作中遇到一個比較棘手的問題,客戶端從服務端同步資料的問題。
背景簡介:客戶端有N個,客戶端上的同步時間,各不相同。同步的時候,是一次獲取10條資料,多批次獲取。即分頁獲取。
在程式碼中存在兩種同步的方式:
- 全量同步。同步過程是從服務端拉取全部的資料;依賴具有
唯一約束
的ID
來實現同步。只適用於資料量小的表,浪費網路流量。 - 增量同步。從伺服器拉取
大於
客戶端最新時間
的資料;依賴於時間戳
,問題時間戳不唯一
存在相同時間點下面多條資料,會出現資料遺漏,也會重複拉取資料,浪費網路流量。
本文的所使用到的解決辦法,就是結合了 唯一ID 和 時間戳 ,兩個入參來做增量同步。本文也只做邏輯層面的說明。
模擬場景
表結構:ID 具有唯一約束, Name 姓名, UpdateTime 更新時間;現在問題的關鍵是ID為3,4,這兩條時間點相同的資料。
假如一次只能同步一條資料,如何同步完ID 2後,再同步 ID 3。
ID | Name | UpdateTime |
---|---|---|
1 | 張三 | 2018-11-10 |
2 | 李四 | 2018-12-10 |
3 | 王五 | 2018-12-10 |
4 | 趙六 | 2018-11-20 |
5 | 金七 | 2018-11-30 |
解決思路
生成新的唯一標識
通過 UpdateTime 和 ID 這兩種資料,通過某種運算,生成新的數。而這個 新的數
具備 可排序 和 唯一 ;同時還要攜帶有 ID
和 UpdateTime
的資訊。
簡單表述就是,具有一個函式f: f(可排序A,可排序唯一B) = 可排序唯一C 。 C 的唯一解是 A和B。 RSA加密演算法
我想出了一個方法,也是生活中比較常用的方法:
- 先把 UpdateTime 轉變成數字。如: 字串 2018-12-10 -> 數字 20181210;
- 然後 UpdateTime 乘以權重,這個
權重
必須大於ID
的可能最大值。如: 20181210 * 100 = 2018121000,Max(ID)<999 - 然後再把第二部的結果,加上唯一鍵
ID
。如: 2018121000 + 3 = 2018121003。
這個時候, 2018121003 這個數,既包含了 UpdateTime
和 ID
的資訊,又具有 可排序 和 唯一性 。用它作為增量更新的判斷點,是再好不過的了。
但是它具有很大的缺點:數字太大了,時間轉化成數字,目前還是用的是 天 級別,如果換成 毫秒 級別呢。還有ID可能的最大值也夠大了,如果是 int64 那就更沒得搞了。
這個方法理論上可行,實際中不可用基本不可行,除非找到一種非常好的函式f;
PS: 我的直覺告訴我: 極可能存在這種函式,既滿足我的需要,又可以克服數字很大這個問題。只是我目前不知道。
資料庫表修改(不推薦)
修改資料內容
修改資料內容,使 UpdateTime
資料值唯一。缺點也比較明顯:
- 指令碼操作資料的情況下,或者直接sql更新。可能會,造成時間不唯一;
- 只是適用在資料量小,系統操作頻率小的情況下。因為毫秒級別的時間,在絕大多數軟體系統中,可以認為是唯一;
- 尤其是老舊專案,歷史遺留資料如何處理。
增加欄位
還有一種辦法,就是在資料庫中,增加一個新的欄位,專門用來同步資料的時候使用。
比方說,增加欄位 SyncData
int 型別。如果 UpdateTime 發生了改變,就把它更新為 SyncData = Max(SyncData) + 1
;
也就是說, SyncData
這個欄位的最大值 一定是 最新的資料, SyncData
的降序就是 更新時間的降序。 SyncData
是 更新時間順序
的 充分不必要條件 。
總的來說,這種辦法是比較好的,但缺點也比較明顯:
- 需要修改表結構,並且額外維護這個欄位;
- 新增或者更新的時候,會先鎖表,找出這個表的最大值,再更新,資源浪費明顯。
- 如果表的資料量比較大,或者更新比較頻繁時候。時間消耗較大。
我的解決方法
分頁提取資料的可能情況
首先,先來分析一下,一次提取10條資料,提取的資料,存在的可能情況。再次說明 前提 ,先時間倒序,再ID倒序。 Order By UpdateTime DESC, ID DESC
可能情況如下圖,可以簡化為三種:
- 情景1。當前獲取的資料中包含了, 所有 相同時間點的資料;圖1,圖5
- 情景2。當前獲取的資料中包含了, 部分 相同時間點的資料;圖2,圖3,圖4,圖6,圖7
- 情景3。當前獲取的資料中包含了, 沒有 相同時間點的資料;圖···
情景1
和
情景3
,可以把查詢條件變為:
WHERE UpdateTime > sync_time LIMIT 10
但是 情景2
的情況不能使用大於 >
這個條件。假如使用了大於 >
這個條件, 情景2
就會變成 情景1
或 情景3
或 圖3
這種情況。不是包含 部分 了,需要額外特別處理。
注:圖3的 結束點 ]
不重要,下面情景5有解釋。

情景2部分情況,提取的起始點
提取的起始點:也就是說圖中 [
左中括號 的位置,需要準確定位這個位置。
至於結束點:圖中 ]
右中括號 的位置是在哪裡。這個就不重要了,因為下一次的分頁提取的 起始點
,就是 上一次的結束點 。只需要關注起始點就足夠了。
而根據起始點,又可以把 情景2
,再做一次簡化:
- 情景4。起始點 在 相同時間點集合內的;圖2,圖4,圖6,圖7
- 情景5。起始點 不在 相同時間點集合內的;圖3,
針對 情景4
。這個時候,時間戳 sync_time
一個入參就不夠了,還額外需要 唯一鍵 ID來準確定位。可以把查詢寫作: WHERE UpdateTime = sync_time AND ID > sync_id LIMIT 10
。
如果查詢的行數 等於 10,則是圖4; 小於 10,則是圖2,圖6,圖7的情況。
針對 情景5
。依舊可以使用: WHERE UpdateTime > sync_time LIMIT 10
完整的分頁過程
完整的分頁過程的步驟:
一、先用起始點來過濾: WHERE UpdateTime = sync_time AND ID > sync_id LIMIT 10
,查詢結果行數N。如果 N=10 或 N=0
,則結束,並且直接返回結果。如果 0< N <10
,則進行第二步;
二、再用時間戳查詢: WHERE UpdateTime > sync_time LIMIT 10-N
,查詢結果行數 M , 0<= M <=10-N
;這個階段,是否同一個時間點都不重要了。只需要按著順序取 已排序 的資料就可以了;
三、把一和二的結果集合並,一併返回。
四、重複步驟一二三,直到,分頁獲取的最後一條資料的 ID
,是服務端資料庫中最新的ID;(防止存在,恰好這十條是所需要獲取的最後十條)。
服務端中最新ID獲取: Select Id From myTable Order by UpdateTime desc,ID desc Limit 1
;
經驗總結
尋找 關鍵資訊 ,以及具有 指標意義 的資料,或者 資料的組合 。
- 最開始,我只執著於 UpdateTime 這個資料,甚至提出去資料庫中,修改歷史資料,再把 UpdateTime 加上唯一約束(以前也沒有聽說過在 UpdateTime 這個欄位上面加唯一約束)。並且這種辦法,侷限性有很強,不可以通用。
- 主鍵ID唯一,但是它不具有時間屬性。只適用於全部更新。
- 把他們兩個結合起來,才算是打開了新的思路。
拆分問題, 簡化 問題
- 把 UpdateTime 和 ID 組合使用時。妄圖在一個sql裡面來實現。發現無論怎麼改,都會存在邏輯上面的問題;
- 沒有拆分化簡的時候,如果用儲存過程來寫的話,會非常非常複雜;
- 直到,我在腦袋裡面,模擬出來可能的情況後。也就是上面的圖片
同步資料的可能性
,慢慢歸類,簡化後;才發現。問題沒有那麼難,僅僅是 起始點 這一個小小的問題。
使用 邏輯分析 和 哲學歸納
- 在分析資料的意義和性質的時候,偶然間使用到了歸納的方法;也就是
唯一
和可排序
;跳出了具體欄位,使用場景的框架束縛,而去考慮這兩種性質怎麼結合的問題; - 在邏輯分析的時候,先用排列組合,算出多少種可能性;在腦中勾畫出圖形,把性質相同的可能性合併化簡;
- 在化簡的過程中,不要僅僅著眼於查詢的物件,也要去化簡
查詢的方法
;有點繞,打個比方,既要優化最終產品,也要去優化製作工藝;
最後,我認為我最近的邏輯分析能力,好像有比較大的提升。
- 直接得益於,常見的24種邏輯謬誤的瞭解, 【轉】邏輯謬誤列表(序言) ,在平常的生活中,說話做事,也就有了邏輯方面的意識;
- 間接可能得益於臺大哲學系苑舉正,苑老師講話的視訊。其實我很早以前,高中時候就喜歡哲學,《哲學的基本原理》這麼枯燥的書,我居然認認真真仔仔細細的邊讀邊想的看了三四遍。只是那時好多完全不懂,好多似懂非懂。十多年後雖然什麼都不記得了,但是好像又懂了。。。感覺太玄了。。。