仔細考慮同步,否則同步將是痛苦。
不是蘇斯博士說的 (蘇斯博士是美國著名兒童文學和圖書作家)
同步是軟件開發(fā)中的一項基本要素。它包括很多種形式,從強制使用不同設備上的時鐘來協(xié)商它們之間的延遲,到使用 @synchronized
代碼塊來序列化訪問多線程編程中的資源。
本文,我將要介紹多種 數據同步 的方法,接下來本文將使用 同步 (sync) 代替數據同步 (data synchronization)。簡單來說,問題就在于:如何存儲時間和空間上分離的兩部分數據,讓這兩部分的數據盡可能的相同。
我個人的興趣要追溯到早期的 iOS App Store,那時,同步在我的生活中扮演了重要的角色。當時,我是學習卡片應用程序 Mental Case 的開發(fā)者。由于包括 Mac 版、 iPad 版和 iPhone 版, Mental Case 更像是一套而不是單個應用,并且它的一大特色就是能夠在不同的設備之間同步你的學習資料。最初,在以數據為中心的年代。 Mental Case 將 Mac 作為中心,通過本地 Wi-Fi 網絡,和一個或者多個 iOS 設備同步數據?,F(xiàn)在, Mental Case 系列應用通過 iCloud 進行點到點 (peer-to-peer) 的數據同步。
合理地實現(xiàn)數據同步是有挑戰(zhàn)的,但是更大的問題是專業(yè)化而不是開發(fā)一個通用的 Web 服務,然后這就要考慮更專業(yè)的解決方案。比如,當一類 Web 服務總是需要服務端的開發(fā)的時候,使用一種同步框架能夠在你現(xiàn)有的代碼基礎上做最少的改變,并且完全不需要服務端的代碼。
在下文中,我將介紹在移動設備早期出現(xiàn)的多種數據同步的方法,在高級層面上解釋它們的工作原理,并給出一些他們的最佳使用指導。我還將根據當今的形勢,描繪一些數據同步領域的新趨勢。
在開始介紹多種數據同步方法的細節(jié)之前,有必要了解它的演變過程以及如何適應早期的技術帶來的限制的。
就消費設備而言,數據同步始于有線連接。上世紀 90 年代末和 21 世紀初,像 Palm Pilot 和 iPod 這樣的外圍設備能夠通過火線 (Firewire) 或者 USB 和 Mac 或者 PC 進行同步。蘋果的數字中心策略正是基于這種方法。后來,由于網速的提升,Wi-Fi 和藍牙在一定程度上增補了有線連接,但是 iTunes 現(xiàn)在仍然使用這種方式。
由于 21 世紀云服務的飛速發(fā)展,由 Mac 或者 PC 作為中心的方式已經逐步轉向了云。云的優(yōu)勢在于無論什么時候,只要設備有網絡,它就可以使用。有了基于云的數據同步,你再也不用呆在家里的電腦旁邊進行同步數據了。
上面提到的每一種方式都在設備間利用了我稱之為 同步通訊 (Synchronous Communication, SC)的概念。你的 iPhone 上的一個應用直接和一臺 Mac 或者云服務通訊,然后實時地接收返回的數據。
現(xiàn)在,出現(xiàn)了一種新興的基于 異步通訊 (Asynchronous Communication, AC) 的數據同步方式。應用不再直接和云“通話”,而是和一個框架或者本地的文件系統(tǒng)交換數據。應用程序不再期望立刻得到回應,取而代之的是,數據是在后臺和云端進行交互了。
這種方式將應用程序代碼和數據同步過程解耦,將開發(fā)者從精確操縱數據同步中解放出來。遵循這一新趨勢的產品范例有蘋果的 Core Data-iCloud 框架,Dropbox Datastore API,甚至像 TouchDB (基于 CouchDB project)那樣的文件存儲。
數據同步的這段歷史并不是遵循一個單一的線性路徑。每個階段都是先遵循,后使用,再創(chuàng)新的更迭演化。今天,所有的這些技術仍然存在并且仍在使用,并且他們中的每一個都有可能是你的某個特定問題的合適的解決方案。
我們已經知道數據同步的方式可以根據它們是否使用了同步通訊來分類,但是也能夠根據與客戶端交互是否使用了“智能”服務器,或者同步過程是否采用以客戶端處理復雜事情的對等方式。下面這個簡單的表格列出了所有的同步技術:
同步 | 異步 | |
客戶端-服務端 |
Parse StackMob Windows Azure Mobile Services Helios Custom Web Service |
Dropbox Datastore TouchDB Wasabi Sync Zumero |
對等方式 |
iTunes/iPod Palm Pilot |
Core Data with iCloud TICoreDataSync Core Data Ensembles |
同步對等網絡 (Synchronous Peer-to-Peer, S-P2P) 是實際上第一個被廣泛接收的方式,并被用于像 iPod 和 PDA 這樣的外圍設備。S-P2P 實現(xiàn)簡單并且本地網絡速度快。由于 iTunes 需要傳輸大量的媒體介質,所以 iTunes 仍然使用這種方式。
http://wiki.jikexueyuan.com/project/objc/images/10-1.png" alt="" />
Synchronous Peer-to-Peer (S-P2P)
同步客戶端服務器 (Synchronous Client-Server, S-CS) 方式隨著網絡的發(fā)展以及像亞馬遜云服務 (AWS) 這樣的云服務的流行而變得流行起來。S-CS 可能是當今最常用的同步方式。站在實現(xiàn)的立場上,它和開發(fā)任何其他的 web 服務非常相似。典型地,一個自定義的云應用程序使用某種語言開發(fā),該全棧式開發(fā)框架可能和客戶端程序無關,比如 Ruby on Rails, Django,或者 Node.js。與云通訊的速度要比本地網絡慢,但是 S-CS 有一個優(yōu)勢叫做“始終在線”,因此,只要網絡保持連接,客戶端可以在任何位置同步數據。
http://wiki.jikexueyuan.com/project/objc/images/10-2.png" alt="" />
Synchronous Client-Server (S-CS)
對于異步客戶端服務器 (Asynchronous Client-Server, A-CS) 方式,開發(fā)者使用數據存儲的 API,存取本地備份的數據。同步數據的過程透明地發(fā)生在后臺,應用程序代碼通過回調機制被告知是否發(fā)生變化。采用這種方式的的例子包括 Dropbox Datastore API,以及對 Core Data 開發(fā)者來說的 Wasabi Sync 服務。
異步 冗余同步 方式的一個優(yōu)點是當網絡不可用的時候,應用程序可以繼續(xù)工作并能夠存取用戶數據。另一個優(yōu)點是開發(fā)者不用再關注通訊和同步的細節(jié),可以集中精力在應用程序的其他方面,數據存儲看上去就好像是在設備本地進行的。
http://wiki.jikexueyuan.com/project/objc/images/10-3.png" alt="" />
Asynchronous Client-Server (A-CS)
異步對等方式 (Asynchronous Peer-to-Peer, A-P2P) 目前尚在初期,并且沒有被廣泛地使用。A-P2P 將所有的負載分發(fā)到客戶端程序上,并且不使用直接通訊。開發(fā)一個 A-P2P 框架是比較復雜的,并且導致了一些眾所周知的問題,包括蘋果早期想讓 iCloud 支持 Core Data (現(xiàn)在已經支持的很好了)。和 S-CS 一樣,每一個設備都有一份數據存儲的完整副本。通過交換不同設備之間的文件的變化來實現(xiàn)數據同步,這些文件通常被稱為 事務日志 。事務日志被上傳到云端,然后從云端通過一個基本的文件處理服務器 (比如 iCloud, Dropbox) 分發(fā)給其他設備,這一過程不需要知道日志的具體內容。
http://wiki.jikexueyuan.com/project/objc/images/10-4.png" alt="" />
Asynchronous Peer-to-Peer (A-P2P)
鑒于開發(fā) A-P2P 系統(tǒng)的復雜性,你也許會問為什么我們還要自找麻煩地去開發(fā)。A-P2P 框架的一個主要優(yōu)勢是它抽離了對智能服務器的需求。開發(fā)者能夠不用考慮服務端的開發(fā),并可以利用多種可用的文件傳輸服務的優(yōu)勢,他們其中的大多數是免費的。而且,由于 A-P2P 系統(tǒng)不連接到一個特定的服務,也就沒有了供應商被鎖定的危險。
介紹完了不同種類的數據同步算法,我現(xiàn)在想介紹一下這些算法的常用組件。你可以想一想如何操控一個孤立的應用程序。
所有的同步方法都有一些共同的要素,包括:
在接下來的部分,在繼續(xù)介紹如何實現(xiàn)這些算法細節(jié)之前,我想先介紹這些要素。
在只有一個數據存儲的獨立應用程序中,對象的識別典型地可以使用數據庫表的行索引,或者在 Core Data 中與之類似的東西,比如 NSManagedObjectID
,這些識別的方法只特定地適用于本地存儲,并不適合在不同的設備之間識別相應的對象。當應用程序同步的時候,很重要的一點就是在不同存儲中的對象能夠與其他的對象相互關聯(lián),因此需要 全局標識符 。
全局標識符通常就是 Universally Unique Identifiers (UUIDs);不同存儲中的對象,如果具有相同全局標識符,則可以認為在邏輯上代表一個單一實例。對一個對象的修改最后會導致相應的對象也會被更新。(UUIDs 可以由 Cocoa 最近添加的 NSUUID
類來創(chuàng)建,或者經常被遺忘的 NSProcessInfo
類的 globallyUniqueString
方法)。
UUIDs 并不是對所有的對象都適用。比如,它就不太適合那些有固定成員集合的類的對象。一個一般的例子是一個單例對象,只有一個可能的對象被允許。另外一個例子是唯一表示是字符串的類標簽 (tag-like) 對象。
但是一個類決定了對象標識,重要的是它能在全局標識符中被反映出來。邏輯上相同的對象在不同的存儲中應該具有相同的標識符,并且不相同的對象應該具有不相同的標識符。
變化追蹤 用來描述同步算法如何確定自上一次同步后,數據發(fā)生了哪些變化,然后本地存儲應該如何修改。對象的每一次修改 (通常稱為 增量) 通常是一個 CRUD 操作:創(chuàng)建 (creation),讀取 (read),更新 (update),刪除 (deletion)。
我們面臨的第一個選擇就是要選擇記錄粒度的大小。當任何單個屬性變化的時候,是應該更新實體里的全部屬性呢?還是只記錄被修改的屬性。正確的選擇也許不同;我將會在研究細節(jié)的時候更多地討論這一話題。
在任何一種情況下,你需要一種方法來記錄變化。在最簡單的情形下,本地存儲里可能就是一個 Boolean
型屬性來標識一個對象是不是新的,或者自上一次更新后有沒有被更新。在更高級的算法中,變化被記錄在主存儲之外,以字典的方式記錄被修改的屬性并有一個與之相關聯(lián)的時間戳。
當邏輯上相同的數據集有兩個或更多的存儲時,潛在的 沖突 就可能出現(xiàn)。沒有同步的情況下,修改一個存儲里某個對象,與修改另一個存儲中與之相對應的對象,這兩件事可能同時發(fā)生。這些改變同時發(fā)生,一些行為可能就會留下一些沖突的對象,一旦數據同步,合法的狀態(tài)就會出現(xiàn)在所有的存儲中。
在最簡單的世界里,讀寫存儲可以被認為是原子操作,因此解決沖突就可以簡單地看成選擇什么版本的存儲。這也許比你想的要普通的多。比如,iCloud 對文檔的同步就是用的這種方式:當發(fā)生沖突的時候,將詢問用戶希望存儲哪個版本 — 這沒有合并有沖突的存儲之間的變化。
當解決沖突的時候,有很多種方法來決定優(yōu)先考慮哪些變化。如果你使用了一個中央服務器,那么最直接的方式就是假設最近的一次同步操作級別最高。所有在這一次同步操作中的變化將覆蓋之前存儲的數據。復雜一點的系統(tǒng)會比較沖突發(fā)生的時候的時間戳然后選擇最近的一次。
沖突解決可能會比較棘手,如果你已經有了選擇,你應該避免設計一個模型僅僅是讓它們變得合法。在一個新的項目中,這比思考所有可能出現(xiàn)的的無效狀態(tài)要容易的多。
關系可能是非常麻煩的 (這可不是對人際交往的評價)。拿一個實體 A
和實體 B
之間簡單的一對一的關系舉例。假設 設備1
和 設備2
都擁有對象 A[1]
,和與之相關的對象 B[1]
。設備1
創(chuàng)建了一個對象 B[2]
,并將 A[1]
和 B[2]
相關聯(lián),然后刪除 B[1]
。同時,設備2
也刪除 B[1]
,但是創(chuàng)建了 B[3]
,并將 B[3]
和 A[1]
關聯(lián)。
http://wiki.jikexueyuan.com/project/objc/images/10-5.png" alt="" />
Orphaned object arising from conflicting changes to a one-to-one relationship.
同步之后,將會出現(xiàn)一個額外的,孤立的,不和任何對象 A 相關聯(lián)的對象 B。如果關系需要一個驗證規(guī)則,那么你就得到了一個無效對象圖。而這僅僅是你能想到的最簡單的一種關系。當涉及到更復雜的關系時,還可能會出現(xiàn)更多的問題。
但是這樣的沖突都解決了,對我們的信息還是有決定性重大幫助的。如果同樣的場景發(fā)生在兩臺不同的設備上,就應該使用同樣的辦法解決。
這看上去可能顯而易見,但是很容易出錯。還是上面那個例子,如果你的方案是隨機的選擇對象 B
中的一個刪除,在某種情況下,兩個設備可能會刪除不同的對象,那么最后就完全沒有對象 B
了。你應該力爭在每個設備中刪除對應的對象 B
。這是可以實現(xiàn)的,可以首先對對象排序,然后總是選擇相同的對象。
既然我們已經了解了所有同步算法的基本要素,接下來,就更詳細的看一看之前介紹的每一種特定的方式,首先介紹同步 (SC) 通訊方法。
我們從最簡單的可工作的 S-P2P 方案開始。假設我們有一個像 iTunes 那樣的 Mac 應用程序,它可以通過 USB、藍牙或者 Wi-Fi 和 iPhone 進行同步通訊。憑借快速的本地網絡,我們不用太在意限制數據傳輸,所以我們可以在這方面偷點懶。
當 iPhone 第一次同步的時候,兩個應用程序通過 Bonjour 發(fā)現(xiàn)對方,然后 Mac 應用程序將它的所有存儲數據壓縮,通過套接字將壓縮后的文件傳遞給 iPhone 應用程序,然后 iPhone 將其解壓并安裝。
現(xiàn)在假設用戶使用 iPhone 對已經存在的對象做了修改 (比如,給一首歌打了星級)。該設備上的應用程序給該對象設置了一個 Boolean 型的標記(比如,changedSinceSync
),用來表示該對象是新的還是已經被修改過的。
當下一次同步發(fā)生的時候,iPhone 應用程序將它的所有數據存儲壓縮并回發(fā)給 Mac。Mac 裝載這些數據,尋找被修改的實例,然后更新它自己對應的數據。然后 Mac 又將更新后的數據存儲的完整拷貝發(fā)送給 iPhone,用來替代 iPhone 已經存在的數據存儲,然后整個流程又重新開始。
雖然還有很多變化和改進的可能,但是這的確是一個可行的方案,并且適用于很多應用程序。總結來說,同步操作需要設備能夠向其他設備傳輸數據,并且能夠決定哪些被修改、合并,然后回傳更新后的數據。你保證了這兩個設備同步之后具有相同的數據,所以,有很強的健壯性。
當等式中加入了服務器的時候,事情變得微妙起來。服務器是為了能夠更加靈活地同步數據,但是它是以數據傳輸和存儲為代價的。我們需要盡可能地減少通訊開銷,所以來回地拷貝整個數據是不可行的。
這一次,我還是把重點放在最簡單可行的方案上。假設數據存儲在服務器上的數據庫中,并且每一個對象都有一個最后更新的時間戳。當客戶端程序第一次同步的時候,它以序列化 (比如 JSON)的形式下載所有的數據,然后建立一個本地存儲。它同樣也在本地記錄了同步的時間戳。
當客戶端程序發(fā)生改變的時候,它會更新對象的最后更新時間戳。服務器也會做同樣的事情,其他設備也應該在這個過渡期里同步。
當下一次同步發(fā)生的時候,客戶端會決定自上一次同步后,哪些對象做了修改,然后僅把被修改的對象發(fā)送給服務器。服務器會合并這些修改。如果服務器對某一個對象的拷貝被另一個客戶端做了修改,那么它會以最近的時間戳為準來保存修改。
然后服務器會回傳所有比上一次從客戶端發(fā)來的時間戳新的變化。這需要考慮到合并的問題,刪除所有覆蓋的變化。
也許有很多不同的方法。比如,你可以為每一個個人屬性引入一個時間戳,然后在粒度級去追蹤變化?;蛘吣憧梢栽诳蛻舳撕喜⑺械臄祿?,然后將合并后的結果發(fā)回給服務器,這實際上是互換了角色。但是,基本說來,一個設備發(fā)送修改結果給其他設備,然后接收方合并并回發(fā)合并后的結果。
刪除需要考慮更多。因為一旦你刪除了一個對象,你就不可能跟蹤它了。一種選擇是使用 軟刪除 ,也就是對象并不是被真正的刪除,而是標記為刪除 (比如使用一個 Boolean 屬性)。(這和在 Finder 中刪除一個文件類似。只有當你清空的垃圾桶之后,它才被永久地刪除。)
異步的數據同步框架和服務的吸引力在于它們提供了現(xiàn)成的解決方案。上文提到的同步的數據同步方案是要定制的—也就是說你不得不為每一個應用程序寫很多的自定義代碼。另外,使用 S-CS 架構,你不得不在所有的平臺間復制類似的功能,來保持服務器的操作。而這需要的技能是大多數 Objective-C 開發(fā)者所不具備的。
異步服務 (比如, Dropbox Datastore API 和 Wasabi Sync 通常提供的框架,讓應用程序開發(fā)者用起來好像是本地數據存儲。這些框架在本地保存修改,然后在后臺控制與服務器的同步。
A-CS 和 S-CS 的一個最主要的區(qū)別在于,A-CS 框架額外提供的抽象層,屏蔽了直接參與同步的客戶端代碼。這也意味著,同一服務可以用于所有的數據模型,而不是特定的一種模型。
A-P2P 是最沒有被充分開發(fā)的方式,因為它也是最難實現(xiàn)的。但是它的承諾是偉大的,因為它比 A-CS 在后端更加抽象,使得一個獨立的應用程序能夠通過不同的服務進行同步。
盡管沒有被充分開發(fā),但還是有應用程序已經在使用這種方式。比如,著名的待辦事項軟件 Clear 就自己實現(xiàn)了 A-P2P,通過 iCloud 進行同步,并且有在線文檔。還有一些框架像蘋果的 Core Data—iCloud 集成, TICoreDataSync 以及 Core Data Ensembles 均采用這種方式并且逐漸被使用。
作為一個應用程序開發(fā)者,你不需要過多關心一個 A-P2P 系統(tǒng)是如何工作的 — 錯綜復雜的事物應該盡可能被隱藏起來 — 但是還是值得在基本層面了解它是如何工作的,以及所涉及的各種挑戰(zhàn)。
在最簡單的情形下,每一個設備將它的 CRUD 修改保存到事務日志文件中,然后將它們上傳到云端。每一個修改都包括一個有序參數,比如一個時間戳,然后當設備從其他設備接收到新的更改時,作為回應,它會建立一個數據存儲的本地拷貝。
如果每一個設備一直寫事務日志,云端的數據會 無限制地 增長。重定基準技術可以用來壓縮舊的變化集然后設置一個新的 基準線 。實際上,由所有舊變化的結束到新對象的產生代表了存儲的初始化狀態(tài)。這減少了歷史遺留的冗余的日志。比如,如果刪除了一個對象,所有與這個對象相關的修改都被刪除了。
這一段簡短的描述也許使 A-P2P 看起來是簡單的算法,但是上面的描述隱藏了許多許多復雜的東西。A-P2P 是復雜的,甚至比其他數據同步的形式都要復雜
A-P2P 最大的一個風險是發(fā)散 (divergence)。由于沒有中央服務器,沒有不同設備間的直接通訊,隨著時間的推移,一個不良的實現(xiàn)很容易導致不一致性。(我敢打賭,作為一個應用程序開發(fā)者,你絕對不想處理像蝴蝶效應那樣的問題。)
如果你能保證在云端永久存儲著全部數據存儲的最新副本,A-P2P 也就沒那么難了。但是,每一次存儲都拷貝數據需要大量的數據傳輸,所以 A-P2P 的應用程序需要以塊為單位接收數據,而且它們也不能及時的知道其他數據和設備。修改甚至會不按順序到達,或者期望從其他設備發(fā)來的修改還沒有來到。你可以從字面上期望看到還沒有被創(chuàng)建的對象發(fā)生的改變。
不僅僅是變化可能無序到達,甚至決定順序應該是怎樣的都是有挑戰(zhàn)性的。時間戳通常是不可信的,特別是在 iPhone 這樣的客戶端上。如果你不小心,接受了一個將來時間的時間戳,這可能會使你不能添加新的改變。使用更健壯的方式使事件及時按序到達是可行的 (比如,Lamport Timestamps 和 Vector Clocks),但是還是有代價的:那就只能近似的使事件及時按序地到達。
類似這樣的細節(jié)還有很多,他們都給 A-P2P 的實現(xiàn)帶來了挑戰(zhàn)。但是那不意味著我們不要應該嘗試?;貓蟆蠖宋粗耐酱鎯Α怯袃r值的目標,而且能夠降低在應用程序中實現(xiàn)同步的困難。
我經常聽到人們說同步是一個已經解決了的問題。我多么希望事實如聽上去那樣簡單,因為那樣的話每一個應用程序都會支持同步。事實上,只有很少的應用程序支持同步。更準確來說,同步的方案不易被接納,代價高,或者在某些方面受限。
我們已經知道數據同步算法有很多不同的形式,而且確實沒有普適的方法。你使用的方案取決于你的應用程序的需要,你的資源,以及你的編程水平。
你的應用是否需要處理大量的媒體數據?除非你有大量的啟動資金,否則你最好在本地網絡使用好用的老式的 S-P2P,就像 iTunes 那樣。
想讓單個數據模型擴展到社交網絡或者實現(xiàn)跨平臺?自定義 Web 服務的 S-CS 也許是一個選擇。
正在開發(fā)一個新的應用程序,重點是要無論在任何地方都能夠同步,但是你又不想花費太多的時間在這方面?那么就使用像 Dropbox Datastore API 這樣的 A-CS 方案吧。
又或者你已經有了一個基于 Core Data 的應用程序,不想和服務器混在一起,而且又不想被某個供應商鎖起來?那么像 Ensembles 這樣的 A-P2P 方案就是你最好的選擇。(好吧,我承認,我是 Ensembles 項目的創(chuàng)立者和主要程序員。)
總之,做選擇的時候,要明智一點兒。:)