解決Android云同步的保存沖突

2018-08-02 17:43 更新

編寫:jdneo - 原文:http://developer.android.com/training/cloudsave/conflict-res.html

這篇文章將介紹當(dāng)應(yīng)用使用Cloud Save service存儲數(shù)據(jù)到云端時,如何設(shè)計(jì)一個魯棒性較高的沖突解決策略。云存儲服務(wù)允許我們?yōu)槊恳粋€在Google服務(wù)上的應(yīng)用用戶,存儲他們的應(yīng)用數(shù)據(jù)。應(yīng)用可以通過使用云存儲API,從Android設(shè)備,iOS設(shè)備或者Web應(yīng)用恢復(fù)或更新這些數(shù)據(jù)。

云存儲中的保存和加載過程非常直接:它只是一個數(shù)據(jù)和byte數(shù)組之間序列化轉(zhuǎn)換,并將這些數(shù)組存儲在云端的過程。然而,當(dāng)用戶有多個設(shè)備,并且有兩個以上的設(shè)備嘗試將它們的數(shù)據(jù)存儲在云端時,這一保存的行為可能會引起沖突,因此我們必須決定應(yīng)該如何處理這類問題。云端數(shù)據(jù)的結(jié)構(gòu)在很大程度上決定了沖突解決方案的魯棒性,所以務(wù)必小心地設(shè)計(jì)我們的數(shù)據(jù)存儲結(jié)構(gòu),使得沖突解決方案的邏輯可以正確地處理每一種情況。

本篇文章從一些有缺陷的解決方案入手,并解釋他們?yōu)楹尉哂腥毕荨V髸尸F(xiàn)一個可以避免沖突的解決方案。用于討論的例子關(guān)注于游戲,但解決問題的核心思想是可以適用于任何將數(shù)據(jù)存儲于云端的應(yīng)用的。

沖突時獲得通知

OnStateLoadedListener方法負(fù)責(zé)從Google服務(wù)器下載應(yīng)用的狀態(tài)數(shù)據(jù)。回調(diào)函數(shù)OnStateLoadedListener.onStateConflict用來給應(yīng)用在本地狀態(tài)和云端存儲的狀態(tài)發(fā)生沖突時,提供了一個解決機(jī)制:

@Override
public void onStateConflict(int stateKey, String resolvedVersion,
    byte[] localData, byte[] serverData) {
    // resolve conflict, then call mAppStateClient.resolveConflict()
 ...
}

此時應(yīng)用必須決定要保留哪一個數(shù)據(jù),或者它自己提交一個新的數(shù)據(jù)來表示合并后的數(shù)據(jù)狀態(tài),解決沖突的邏輯由我們自己來實(shí)現(xiàn)。

我們必須要意識到云存儲服務(wù)是在后臺執(zhí)行同步的。所以我們應(yīng)該確保應(yīng)用能夠在創(chuàng)建這一數(shù)據(jù)的Context之外接收回調(diào)。特別地,如果Google Play服務(wù)應(yīng)用在后臺檢測到了一個沖突,該回調(diào)函數(shù)會在下一次加載數(shù)據(jù)時被調(diào)用,通常來說會是在下一次用戶啟動該應(yīng)用時。

因此,我們的云存儲代碼和沖突解決代碼的設(shè)計(jì)必須是和當(dāng)前Context無關(guān)的:也就是說當(dāng)我們拿到了兩個彼此沖突的數(shù)據(jù),我們必須僅通過數(shù)據(jù)集內(nèi)獲取的數(shù)據(jù)去解決沖突,而不依賴于任何其它任何外部Context。

處理簡單情況

下面列舉一些解決沖突的簡單例子。對于很多應(yīng)用而言,用這些策略或者其變體就足夠解決大多數(shù)問題了:

新的比舊的更有效:在一些情況下,新的數(shù)據(jù)可以替代舊的數(shù)據(jù)。例如,如果數(shù)據(jù)代表了用戶所選擇角色的衣服顏色,那么最近的新的選擇就應(yīng)該覆蓋老的選擇。在這種情況下,我們可能會選擇在云存儲數(shù)據(jù)中存儲時間戳。當(dāng)處理這些沖突時,選擇時間戳最新的數(shù)據(jù)(記住要選擇一個可靠的時鐘,并注意對不同時區(qū)的處理)。

一個數(shù)據(jù)好于其他數(shù)據(jù):在一些情況下,我們是可以有方法在若干數(shù)據(jù)集中選取一個最好的。例如,如果數(shù)據(jù)代表了玩家在賽車比賽中的最佳時間,那么顯然,在沖突發(fā)生時,我們應(yīng)該保留成績最好的那個數(shù)據(jù)。

進(jìn)行合并:有可能通過計(jì)算兩個數(shù)據(jù)集的合并版本來解決沖突。例如,我們的數(shù)據(jù)代表了用戶解鎖關(guān)卡的進(jìn)度,那么我們需要的數(shù)據(jù)就是兩個沖突數(shù)據(jù)的并集。通過這個方法,用戶的關(guān)卡解鎖進(jìn)度就不會丟失了。這里的例子使用了這一策略的一個變形。

為更復(fù)雜的情況設(shè)計(jì)一個策略

當(dāng)我們的游戲允許玩家收集可交換物品時(比如金幣或者經(jīng)驗(yàn)點(diǎn)數(shù)),情況會變得更加復(fù)雜一些。我們來假想一個游戲,叫做“金幣跑酷”,游戲中的角色通過跑步不斷地收集金幣使自己變的富有。每個收集到的金幣都會加入到玩家的儲蓄罐中。

下面的章節(jié)將展示三種在多個設(shè)備間解決沖突的方案:有兩個看上去還不錯,可惜最終還是不能適用于所有情況,最后一個解決方案可以解決多個設(shè)備間的數(shù)據(jù)沖突。

第一個嘗試:只保存總數(shù)

首先,這個問題看上去像是說:云存儲的數(shù)據(jù)只要存儲金幣的數(shù)量就行了。但是如果就只有這些數(shù)據(jù)是可用的,那么解決沖突的方案將會嚴(yán)重受到限制。此時最佳的方案只能是在沖突發(fā)生時存儲數(shù)值最大的數(shù)據(jù)。

想一下表1中所展現(xiàn)的場景。假設(shè)玩家一開始有20枚硬幣,然后在設(shè)備A上收集了10個,在設(shè)備B上收集了15個。然后設(shè)備B將數(shù)據(jù)存儲到了云端。當(dāng)設(shè)備A嘗試去存儲的時候,沖突發(fā)生了?!爸槐4婵倲?shù)”的沖突解決方案會存儲35作為這一數(shù)據(jù)的值(兩數(shù)之間最大的)。

表1. 值保存最大的數(shù)(不佳的策略)

事件設(shè)備A的數(shù)據(jù)設(shè)備B的數(shù)據(jù)云端的數(shù)據(jù)實(shí)際的總數(shù)
開始階段20202020
玩家在A設(shè)備上收集了10個硬幣30202030
玩家在B設(shè)備上收集了15個硬幣30352045
設(shè)備B將數(shù)據(jù)存儲至云端30353545
設(shè)備A嘗試將數(shù)據(jù)存儲至云端,發(fā)生沖突30353545
設(shè)備A通過選擇兩數(shù)中最大的數(shù)來解決沖突35353545

這一策略顯然會失?。和婕业慕饚艛?shù)從20變成35,但實(shí)際上玩家總共收集了25個硬幣(A設(shè)備10個,B設(shè)備15個),所以有10個硬幣丟失了。只在云端存儲硬幣的總數(shù)是不足以實(shí)現(xiàn)一個健壯的沖突解決算法的。

第二個嘗試:存儲總數(shù)和變化值

另一個方法是在存儲數(shù)據(jù)中包括一些額外的數(shù)據(jù),如:自上次提交后硬幣增加的數(shù)量(delta)。在這一方法中,存儲的數(shù)據(jù)可以用一個二元組來表示(T, d),其中T是硬幣的總數(shù),而d是硬幣增加的數(shù)量。

通過這樣的數(shù)據(jù)存儲結(jié)構(gòu),我們的沖突檢測算法在魯棒性上會有更大的提升空間。但是這個方法在某些情況下依然會存在問題。

下面是包含delta數(shù)值的沖突解決算法過程:

  • 本地數(shù)據(jù):(T, d)
  • 云端數(shù)據(jù):(T', d')
  • 解決后的數(shù)據(jù):(T'+d, d)

例如,當(dāng)我們在本地狀態(tài)(T, d)和云端狀態(tài)(T', d)之間發(fā)生了沖突時,可以將它們合并成(T'+d, d)。這意味著我們從本地拿出delta數(shù)據(jù),并將它和云端的數(shù)據(jù)結(jié)合起來,乍一看,這種方法可以很好的計(jì)量多個設(shè)備所收集的金幣。

該方法看上去很可靠,但它在具有移動網(wǎng)絡(luò)的環(huán)境中難以適用:

  • 用戶可能在設(shè)備不在線時存儲數(shù)據(jù)。這些改變會以隊(duì)列形式等待手機(jī)聯(lián)網(wǎng)后提交。
  • 這個方法的同步機(jī)制是用最新的變化覆蓋掉任何之前的變化。換句話說,第二次寫入的變化會提交到云端(當(dāng)設(shè)備聯(lián)網(wǎng)了以后),而第一次寫入的變化就被忽略了。

為了進(jìn)一步說明,我們考慮一下表2所列的場景。在表2列出的一系列操作發(fā)生后,云端的狀態(tài)將是(130, +5),最終沖突解決后的狀態(tài)是(140, +10)。這是不正確的,因?yàn)閺目傮w上而言,用戶一共在A上收集了110枚硬幣而在B上收集了120枚硬幣??倲?shù)應(yīng)該為250。

表2. “總數(shù)+增量”策略的失敗案例

事件設(shè)備A的數(shù)據(jù)設(shè)備B的數(shù)據(jù)云端的數(shù)據(jù)實(shí)際的總數(shù)
開始階段(20, x)(20, x)(20, x)20
玩家在A設(shè)備上收集了100個硬幣(120, +100)(20, x)(20, x)120
玩家在A設(shè)備上又收集了10個硬幣(130, +10)(20, x)(20, x)130
玩家在B設(shè)備上收集了115個硬幣(130, +10)(125, +115)(20, x)245
玩家在B設(shè)備上又收集了5個硬幣(130, +10)(130, +5)(20, x)250
設(shè)備B將數(shù)據(jù)存儲至云端(130, +10)(130, +5)(130, +5)250
設(shè)備A嘗試將數(shù)據(jù)存儲至云端,發(fā)生沖突(130, +10)(130, +5)(130, +5)250
設(shè)備A通過將本地的增量和云端的總數(shù)相加來解決沖突(140, +10)(130, +5)(140, +10)250

注:x代表與該場景無關(guān)的數(shù)據(jù)

我們可能會嘗試在每次保存后不重置增量數(shù)據(jù)來解決此問題,這樣的話在每個設(shè)備上第二次存儲的數(shù)據(jù)就能夠代表用戶至今為止收集到的所有硬幣。此時,設(shè)備A在第二次本地存儲完成后,數(shù)據(jù)將是(130, +110)而不是(130, +10)。然而,這樣做的話就會發(fā)生如表3所述的情況:

表3. 算法改進(jìn)后的失敗案例

事件設(shè)備A的數(shù)據(jù)設(shè)備B的數(shù)據(jù)云端的數(shù)據(jù)實(shí)際的總數(shù)
開始階段(20, x)(20, x)(20, x)20
玩家在A設(shè)備上收集了100個硬幣(120, +100)(20, x)(20, x)120
設(shè)備A將狀態(tài)存儲到云端(120, +100)(20, x)(120, +100)120
玩家在A設(shè)備上又收集了10個硬幣(130, +110)(20, x)(120, +100)130
玩家在B設(shè)備上收集了1個硬幣(130, +110)(21, +1)(120, +100)131
設(shè)備B嘗試向云端存儲數(shù)據(jù),發(fā)生沖突(130, +110)(21, +1)(120, +100)131
設(shè)備B通過將本地的增量和云端的總數(shù)相加來解決沖突(130, +110)(121, +1)(121, +1)131
設(shè)備A嘗試將數(shù)據(jù)存儲至云端,發(fā)生沖突(130, +110)(121, +1)(121, +1)131
設(shè)備A通過將本地的增量和云端的總數(shù)相加來解決沖突(231, +110)(121, +1)(231, +110)131

注:x代表與該場景無關(guān)的數(shù)據(jù)

現(xiàn)在我們碰到了另一個問題:我們給予了玩家過多的硬幣。這個玩家拿到了211枚硬幣,但實(shí)際上他只收集了111枚。

解決辦法:

分析之前的幾次嘗試,我們發(fā)現(xiàn)這些策略存在這樣的缺陷:無法知曉哪些硬幣已經(jīng)計(jì)數(shù)了,哪些硬幣沒有被計(jì)數(shù),尤其是當(dāng)多個設(shè)備連續(xù)提交的時候,算法會出現(xiàn)混亂。

該問題的解決辦法是將我們在云端的數(shù)據(jù)存儲結(jié)構(gòu)改為字典類型,使用字符串+整形的鍵值對。每一個鍵值對都代表了一個包含硬幣的“委托人”,而總數(shù)就應(yīng)該是將所有記錄的值加起來。這一設(shè)計(jì)的宗旨是每個設(shè)備有它自己的“委托人”,并且只有設(shè)備自己可以把硬幣放到它的“委托人”當(dāng)中。

字典的結(jié)構(gòu)是:(A:a, B:b, C:c, ...),其中a代表了“委托人”A所擁有的硬幣,b是“委托人”B所擁有的硬幣,以此類推。

這樣的話,新的沖突解決策略算法將如下所示:

  • 本地數(shù)據(jù):(A:a, B:b, C:c, ...)
  • 云端數(shù)據(jù):(A:a', B:b', C:c', ...)
  • 解決后的數(shù)據(jù):(A:max(a,a'), B:max(b,b'), C:max(c,c'), ...)

例如,如果本地數(shù)據(jù)是(A:20, B:4, C:7)并且云端數(shù)據(jù)是(B:10, C:2, D:14),那么解決沖突后的數(shù)據(jù)將會是(A:20, B:10, C:7, D:14)。當(dāng)然,應(yīng)用的沖突解決邏輯可以根據(jù)具體的需求而有所差異。比如對于有一些應(yīng)用,我們可能希望挑選最小的值。

為了測試新的算法,將它應(yīng)用于任何一個之前提到過的場景。你將會發(fā)現(xiàn)它都能取得正確地結(jié)果。

表4闡述了這一點(diǎn),它使用了表3中所提到的場景。注意下面所列出的關(guān)鍵點(diǎn):

在初始狀態(tài),玩家有20枚硬幣。該數(shù)據(jù)準(zhǔn)確體現(xiàn)在了所有設(shè)備和云端中,我們用字典:(X:20)來代表它,其中X我們不用太多關(guān)心,初始化的數(shù)據(jù)是哪兒來對該問題沒有影響。

當(dāng)玩家在設(shè)備A上收集了100枚硬幣,這一變化會作為一個字典保存到云端。字典的值是100是因?yàn)檫@就是玩家在設(shè)備A上收集的硬幣數(shù)量。在這一過程中,沒有要執(zhí)行數(shù)據(jù)的計(jì)算(設(shè)備A僅僅是將玩家所收集的數(shù)據(jù)匯報給了云端)。

每一個新的硬幣提交會打包成一個與設(shè)備關(guān)聯(lián)的字典并保存到云端。例如,假設(shè)玩家又在設(shè)備A上收集了100枚硬幣,那么對應(yīng)字典的值被更新為110。

最終的結(jié)果就是,應(yīng)用知道了玩家在每個設(shè)備上收集硬幣的總數(shù)。這樣它就能輕易地計(jì)算出實(shí)際的總數(shù)了。

表4. 鍵值對策略的成功應(yīng)用案例

事件設(shè)備A的數(shù)據(jù)設(shè)備B的數(shù)據(jù)云端的數(shù)據(jù)實(shí)際的總數(shù)
開始階段(X:20, x)(X:20, x)(X:20, x)20
玩家在A設(shè)備上收集了100個硬幣(X:20, A:100)(X:20)(X:20)120
設(shè)備A將狀態(tài)存儲到云端(X:20, A:100)(X:20)(X:20, A:100)120
玩家在A設(shè)備上又收集了10個硬幣(X:20, A:110)(X:20)(X:20, A:100)130
玩家在B設(shè)備上收集了1個硬幣(X:20, A:110)(X:20, B:1)(X:20, A:100)131
設(shè)備B嘗試向云端存儲數(shù)據(jù),發(fā)生沖突(X:20, A:110)(X:20, B:1)(X:20, A:100)131
設(shè)備B解決沖突(X:20, A:110)(X:20, A:100, B:1)(X:20, A:100, B:1)131
設(shè)備A嘗試將數(shù)據(jù)存儲至云端,發(fā)生沖突(X:20, A:110)(X:20, A:100, B:1)(X:20, A:100, B:1)131
設(shè)備A解決沖突(X:20, A:110, B:1)(X:20, A:100, B:1)(X:20, A:110, B:1),total 131131

清除你的數(shù)據(jù)

在云端允許存儲數(shù)據(jù)的大小是有限制的,所以在后續(xù)的論述中,我們將會關(guān)注如何避免創(chuàng)建過大的詞典。一開始,看上去每個設(shè)備只會有一條詞典記錄,即使是非常激進(jìn)的用戶也不太會擁有上千種不同的設(shè)備(對應(yīng)上千條字典記錄)。然而, 獲取設(shè)備ID的方法很難,并且我們認(rèn)為這是一種不好的實(shí)踐方式,所以我們應(yīng)該使用一個安裝ID,這更容易獲取也更可靠。這樣的話就意味著,每一次用戶在每臺設(shè)備安裝一次就會產(chǎn)生一個ID。假設(shè)每個鍵值對占據(jù)32字節(jié),由于一個個人云存儲緩存最多可以有128K的大小,因此最多可以存儲4096條記錄。

在現(xiàn)實(shí)場景中,你的數(shù)據(jù)可能更加復(fù)雜。在這種情況下,存儲數(shù)據(jù)的記錄條數(shù)也會進(jìn)一步受到限制。具體而言則需要取決于實(shí)現(xiàn),比如可能需要添加時間戳來指明每條記錄是何時修改的。當(dāng)你檢測到有一條記錄在過去幾個禮拜或者幾個月的時間內(nèi)都沒有被修改,那么就可以安全地將金幣數(shù)據(jù)轉(zhuǎn)移到另一條記錄中并刪除老的記錄。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號