注:本文翻譯自Google官方的Android Developers Training文檔,譯者技術(shù)一般,由于喜愛安卓而產(chǎn)生了翻譯的念頭,純屬個(gè)人興趣愛好。
原文鏈接: http://developer.android.com/training/cloudsave/conflict-res.html
在云存儲(chǔ)中保存和加載過程是很直接的:它只是將用于數(shù)據(jù)和byte數(shù)組之間的序列化轉(zhuǎn)換,并將這些數(shù)組存儲(chǔ)在云端。然而, 當(dāng)你的用戶有多個(gè)設(shè)備,并且兩個(gè)以上的設(shè)備嘗試將它們的數(shù)據(jù)存儲(chǔ)在云端時(shí),這一保存可能會(huì)引起沖突,因此你必須決定應(yīng)該如何處理。你在云端存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)在很大程度上決定了你的沖突解決方案的魯棒性,所以小心地設(shè)計(jì)你的數(shù)據(jù),使得你的沖突檢測解決方案邏輯可以正確地處理每一種情況。
本片文章從描述一些有缺陷的方法入手,并解釋他們?yōu)楹尉哂腥毕荨V蟪尸F(xiàn)一個(gè)解決方案來避免沖突。用于討論的例子關(guān)注于游戲,但解決問題的宗旨是可以適用于任何將數(shù)據(jù)存儲(chǔ)于云端的應(yīng)用的。
一). 沖突時(shí)獲得通知
OnStateLoadedListener
方法負(fù)責(zé)從
Google
服務(wù)器下載應(yīng)用的狀態(tài)數(shù)據(jù)。回調(diào)函數(shù)
OnStateLoadedListener.onStateConflict
為你的應(yīng)用在本地狀態(tài)和云端存儲(chǔ)的狀態(tài)發(fā)生沖突時(shí),
提供了一個(gè)解決機(jī)制:
@Override public void onStateConflict( int stateKey, String resolvedVersion, byte [] localData, byte [] serverData) { // resolve conflict, then call mAppStateClient.resolveConflict() ... }
此時(shí)你的應(yīng)用必須決定要保留哪一個(gè)數(shù)據(jù),或者它自己提交一個(gè)新的數(shù)據(jù)來表示合并后的數(shù)據(jù)狀態(tài)(譯者注:是不是有點(diǎn)像git/svn呢?),解決沖突的邏輯由你來實(shí)現(xiàn)。
我們必須要意識(shí)到云存儲(chǔ)服務(wù)是在后臺(tái)執(zhí)行同步的。所以你應(yīng)該確保你的應(yīng)用能夠在你創(chuàng)建這一數(shù)據(jù)的context之外接收回調(diào)。特別地,如果Google Play服務(wù)應(yīng)用在后臺(tái)檢測到了一個(gè)沖突,該回調(diào)函數(shù)可以在你下一次加載數(shù)據(jù)時(shí)被調(diào)用,而不是下一次用戶啟動(dòng)該應(yīng)用時(shí)。
因此,你的云存儲(chǔ)代碼和沖突解決代碼的設(shè)計(jì)必須是和當(dāng)前context無關(guān)的:即給兩個(gè)沖突的數(shù)據(jù),你必須僅通過數(shù)據(jù)集中獲取的數(shù)據(jù)區(qū)解決沖突,而不依賴于任何其它外部環(huán)境。
二). 處理簡單地情況
下面列舉一些沖突解決的簡單例子。對(duì)于很多應(yīng)用而言,用這些策略或者其變體就足夠解決大多數(shù)問題了:
新的比舊的更有效 :在一些情況下,新的數(shù)據(jù)總是替代老數(shù)據(jù)。例如,如果數(shù)據(jù)代表了用戶選擇角色的衣服顏色,那么最近的新的選擇就應(yīng)該覆蓋老的選擇。在這種情況下,你可能會(huì)選擇在云存儲(chǔ)數(shù)據(jù)中存儲(chǔ)時(shí)間戳。當(dāng)處理這些沖突時(shí),選擇時(shí)間戳最新的數(shù)據(jù)(記住要選擇一個(gè)可靠的時(shí)鐘,并注意對(duì)不同時(shí)區(qū)的處理)。
有一個(gè)數(shù)據(jù)集中的數(shù)據(jù)比其它的更好 :在一些情況下,我們是可以有方法在若干數(shù)據(jù)集中選取一個(gè)最好的。例如,如果數(shù)據(jù)代表了玩家在賽車比賽中的最佳時(shí)間,那么顯然,在沖突發(fā)生時(shí),你應(yīng)該保留成績最好的那個(gè)數(shù)據(jù)。
進(jìn)行合并 :有可能通過計(jì)算兩個(gè)數(shù)據(jù)集的合并版本來解決沖突。例如,如果你的數(shù)據(jù)代表了用戶解鎖關(guān)卡的進(jìn)度,那么解決的數(shù)據(jù)就是沖突集的并集。通過這個(gè)方法,用戶不會(huì)丟失任何他的游戲進(jìn)度。這里的 例子 使用了這一操作的一個(gè)變形。
三). 為更復(fù)雜的情況設(shè)計(jì)一個(gè)策略
一個(gè)更復(fù)雜的情況是當(dāng)你的游戲允許玩家收集可以互換的東西時(shí)(比如金幣或者經(jīng)驗(yàn)點(diǎn)數(shù)),我們來假想一個(gè)游戲,叫做“金幣跑酷”,一個(gè)無限跑步的角色其目標(biāo)是不斷地收集金幣是自己變的富有。每個(gè)收集到的金幣都會(huì)加入到玩家的儲(chǔ)蓄罐中。
下面的章節(jié)將展示三種在多個(gè)設(shè)備間解決沖突的方案:有兩個(gè)聽上去很不錯(cuò),可惜最終還是不能適用于所有的場景,最后一個(gè)解決方案可以解決多個(gè)設(shè)備間的沖突。
第一個(gè)嘗試:只保存總數(shù)
首先,這個(gè)問題看上去像是說:云存儲(chǔ)的數(shù)據(jù)只要存儲(chǔ)金幣的數(shù)量就行了。但是如果就只有這些數(shù)據(jù)是可用的,那么解決沖突的方案將會(huì)嚴(yán)重受到限制。此時(shí)最佳的方案就是在沖突發(fā)生時(shí)存儲(chǔ)最大數(shù)值得數(shù)據(jù)。
想一下表一中所展現(xiàn)的場景。假設(shè)玩家一開始有20枚硬幣,然后再設(shè)備A上收集了10個(gè),在設(shè)備B上收集了15個(gè)。然后設(shè)備B將數(shù)據(jù)存儲(chǔ)到了云端。當(dāng)設(shè)備A嘗試去存儲(chǔ)的時(shí)候,沖突發(fā)生了。“只存儲(chǔ)總數(shù)”的沖突解決方案會(huì)存儲(chǔ)35作為這一數(shù)據(jù)的值(兩數(shù)之間最大的)
表1. 值保存最大的數(shù)(不佳的策略)
事件 | 設(shè)備A的數(shù)據(jù) | 設(shè)備B的數(shù)據(jù) | 云端的數(shù)據(jù) | 實(shí)際的總數(shù) |
開始階段 | 20 | 20 | 20 | 20 |
玩家在A設(shè)備上收集了10個(gè)硬幣 | 30 | 20 | 20 | 30 |
玩家在B設(shè)備上收集了15個(gè)硬幣 | 30 | 35 | 20 | 45 |
設(shè)備B將數(shù)據(jù)存儲(chǔ)至云端 | 30 | 35 | 35 | 45 |
設(shè)備A嘗試將數(shù)據(jù)存儲(chǔ)至云端 發(fā)生沖突 |
30 | 35 | 35 | 45 |
設(shè)備A通過選擇兩數(shù)中最大的數(shù)來解決沖突 | 35 | 35 | 35 | 45 |
這一策略會(huì)失敗——玩家的金幣數(shù)從20變成35,但實(shí)際上玩家總共收集了25個(gè)硬幣(A設(shè)備10個(gè),B設(shè)備15個(gè))。所以有10個(gè)硬幣丟失了。只在云端存儲(chǔ)硬幣的總數(shù)是不足以實(shí)現(xiàn)一個(gè)魯棒的沖突解決算法的。
第二個(gè)嘗試:存儲(chǔ)總數(shù)和變化值
另一個(gè)方法是在存儲(chǔ)數(shù)據(jù)中包括一些額外的數(shù)據(jù):自上次提交后硬幣增加的數(shù)量(delta)。在這一方法中,存儲(chǔ)的數(shù)據(jù)可以用一個(gè)二元組來表示(T, d),其中T是硬幣的總數(shù),而d是硬幣增加的數(shù)量。
在這個(gè)結(jié)構(gòu)中,你的沖突檢測算法在魯棒性上有更大的提升空間,如下將要講的那樣。但是這個(gè)方法還是無法給出一個(gè)可靠的玩家最終的狀態(tài)。
下面是包含delta的沖突解決算法過程:
- 本地?cái)?shù)據(jù): (T, d)
- 云端數(shù)據(jù): (T', d')
- 解決后的數(shù)據(jù): (T'+d, d)
例如,當(dāng)你在本地狀態(tài)( T, d )和云端狀態(tài)( T', d )之間發(fā)生了沖突時(shí),你可以將它們合并成 ( T'+d, d )。意味著你從本地拿出delta數(shù)據(jù),并將它和云端的數(shù)據(jù)結(jié)合起來,乍一看,這種方法可以很好的計(jì)量多個(gè)設(shè)備所收集的金幣。
看上去很可靠的方法,但這個(gè)方法在動(dòng)態(tài)移動(dòng)環(huán)境中難以適用:
- 用戶可能在設(shè)備不在線時(shí)存儲(chǔ)數(shù)據(jù)。這些改變會(huì)以隊(duì)列形式等待手機(jī)聯(lián)網(wǎng)后提交。
- 這個(gè)方法的同步機(jī)制是用最新的變化覆蓋掉任何之前的變化。換句話說,第二次寫入的變化會(huì)提交到云端(當(dāng)設(shè)備聯(lián)網(wǎng)了以后),而第一次寫入的變化就被忽略了。
為了進(jìn)一步說明,我們考慮一下表2所列的場景。在表2的一系列操作后,云端的狀態(tài)將是(130, +5),之后最終沖突解決后的狀態(tài)時(shí)(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ù)據(jù)? |
?開始階段 | ?(20, x) | ?(20, x) | (20, x)? | 20? |
?玩家在A設(shè)備上收集了100個(gè)硬幣 | ?(120, +100) | ?(20, x) | (20, x)? | 120? |
?玩家在A設(shè)備上又收集了10個(gè)硬幣 | ? (130, +10) | ?(20, x) |
(20, x)
|
130? |
?玩家在B設(shè)備上收集了115個(gè)硬幣 | (130, +10) |
(125, +115)
|
(20, x) | 245? |
?玩家在B設(shè)備上又收集了5個(gè)硬幣 | (130, +10) | (130, +5)? |
(20, x)
|
250? |
?設(shè)備B將數(shù)據(jù)存儲(chǔ)至云端 | (130, +10) | (130, +5)? | (130, +5) | 250? |
?
設(shè)備A嘗試將數(shù)據(jù)存儲(chǔ)至云端 發(fā)生沖突 |
(130, +10) ? | (130, +5)? | (130, +5)? | 250? |
?設(shè)備A通過將本地的增量和云端的總數(shù)相加來解決沖突 | ?(140, +10) | (130, +5)? | (140, +10) ? | 250? |
注:x代表與該場景無關(guān)的數(shù)據(jù)
你可能會(huì)嘗試在每次保存后不重置增量數(shù)據(jù)來解決此問題,這樣的話在每個(gè)設(shè)備上的第二次存儲(chǔ)所收集到的硬幣將不會(huì)產(chǎn)生問題。這樣的話設(shè)備A在第二次本地存儲(chǔ)完成后,數(shù)據(jù)將是( 130, +110 )而不是( 130, +10 )。然而,這樣做的話就會(huì)發(fā)生如表3所述的情況:
表3. 算法改進(jìn)后的失敗案例
事件 | 設(shè)備A的數(shù)據(jù) | 設(shè)備B的數(shù)據(jù)? | 云端的數(shù)據(jù) | 實(shí)際的數(shù)據(jù) |
?開始階段 | (20, x) | (20, x) | (20, x) | 20 |
?玩家在A設(shè)備上收集了100個(gè)硬幣 | (120, +100) | (20, x) | (20, x) | 120 |
設(shè)備A將狀態(tài)存儲(chǔ)到云端 | (120, +100) | (20, x) | (120, +100) | 120 |
?玩家在A設(shè)備上又收集了10個(gè)硬幣 | (130, +110) | (20, x) | (120, +100) | 130 |
?玩家在B設(shè)備上收集了1個(gè)硬幣 | (130, +110) | (21, +1) | (120, +100) | 131 |
?設(shè)備B嘗試向云端存儲(chǔ)數(shù)據(jù) 發(fā)生沖突 |
(130, +110) | (21, +1) | (120, +100) | 131 |
?設(shè)備B通過將本地的增量和云端的總數(shù)相加來解決沖突 | (130, +110) | (121, +1) | (121, +1) | 131 |
?
設(shè)備A嘗試將數(shù)據(jù)存儲(chǔ)至云端 發(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)在你碰到了另一個(gè)問題:你給予了玩家過多的硬幣。這個(gè)玩家拿到了211枚硬幣,但實(shí)際上他只收集了111枚。
解決辦法:
我們分析之前的幾次嘗試,我們發(fā)現(xiàn)這些策略都沒有這樣一個(gè)能力:知曉哪些硬幣已經(jīng)計(jì)數(shù)了,哪些硬幣沒有被計(jì)數(shù),尤其是當(dāng)多個(gè)設(shè)備連續(xù)提交的時(shí)候,算法會(huì)出現(xiàn)混亂。
該問題的解決辦法將你云端的存儲(chǔ)結(jié)構(gòu)改為字段,使用字符串+整形的鍵值對(duì)。每一個(gè)鍵值對(duì)都會(huì)代表一個(gè)包含硬幣的“委托”,而總數(shù)就應(yīng)該是將所有值加起來。這一設(shè)計(jì)的宗旨是每個(gè)設(shè)備有它自己的委托,并且只有設(shè)備自己可以吧硬幣放到其委托中。
字典的結(jié)構(gòu)是: (A:a, B:b, C:c, ...) ,其中a代表了委托A所擁有的硬幣,b是委托B所擁有的硬幣,以此類推。
這樣的話,新的沖突解決策略算法將如下所示:
本地?cái)?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'), ...)
例如,如果本地?cái)?shù)據(jù)是 (A:20, B:4, C:7) 并且云端數(shù)據(jù)是 (B:10, C:2, D:14) ,這樣的話解決沖突后的數(shù)據(jù)將會(huì)是 (A:20, B:10, C:7, D:14) 。注意,你應(yīng)用的沖突解決邏輯會(huì)根據(jù)具體的場景可能有所差異。比如,有一些應(yīng)用你可能希望挑選最小的值。
為了測試新的算法,將它應(yīng)用于任何一個(gè)之前提到過的場景。你將會(huì)發(fā)現(xiàn)它都能取得正確地結(jié)果。
表4闡述了這一點(diǎn),它基于表3的場景。注意下面所列的:
在初始狀態(tài),玩家有20枚硬幣。此數(shù)值在所有設(shè)備和云端都是正確的,我們用(X:20)這一元祖代表它,其中X我們不用太多關(guān)心,我們不去追求這個(gè)初始化的數(shù)據(jù)是哪兒來的。
當(dāng)玩家在設(shè)備A上收集了100枚硬幣,這一變化會(huì)作為一個(gè)元組保存到云端。它的值是100是因?yàn)檫@就是玩家在設(shè)備A上收集的硬幣數(shù)量。在這一過程中,沒有要執(zhí)行數(shù)據(jù)的計(jì)算——設(shè)備A僅僅是將玩家所收集的數(shù)據(jù)匯報(bào)給了云端。
每一個(gè)新的硬幣提交會(huì)打包成一個(gè)于設(shè)備關(guān)聯(lián)的元組并保存到云端。例如,假設(shè)玩家又在設(shè)備A上收集了100枚硬幣,那么元組的值被更新為110。
最終的結(jié)果就是,應(yīng)用知道了玩家在每個(gè)設(shè)備上收集硬幣的總數(shù)。這樣它就能輕易地計(jì)算總數(shù)了。
表4. 鍵值對(duì)策略的成功應(yīng)用案例
事件 | 設(shè)備A的數(shù)據(jù) | 設(shè)備B的數(shù)據(jù) | 云端的數(shù)據(jù) | 實(shí)際的數(shù)據(jù) |
開始階段 | (X:20, x) | (X:20, x) | (X:20, x) | 20 |
玩家在A設(shè)備上收集了100個(gè)硬幣 | (X:20, A:100) | (X:20) | (X:20) | 120 |
設(shè)備A將狀態(tài)存儲(chǔ)到云端 | (X:20, A:100) | (X:20) | (X:20, A:100) | 120 |
玩家在A設(shè)備上又收集了10個(gè)硬幣 | (X:20, A:110) | (X:20) | (X:20, A:100) | 130 |
玩家在B設(shè)備上收集了1個(gè)硬幣 | (X:20, A:110) | (X:20, B:1) | (X:20, A:100) | 131 |
設(shè)備B嘗試向云端存儲(chǔ)數(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ù)存儲(chǔ)至云端 發(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 131 |
131 |
四). 清除你的數(shù)據(jù)
在云端存儲(chǔ)數(shù)據(jù)的大小是由限制的,所以在后續(xù)的論述中,我們將會(huì)關(guān)注與如何避免創(chuàng)建過大的詞典。一開始,看上去每個(gè)設(shè)備只會(huì)有一個(gè)詞典字段,即使是非常激進(jìn)的用戶也不太會(huì)擁有上千條字段。然而, 獲取設(shè)備ID的方法很難,并且我們認(rèn)為這是一種不好的實(shí)踐方式,所以你應(yīng)該使用一個(gè)安裝ID,這更容易獲取也更可靠。這樣的話就意味著,每一次用戶在每臺(tái)設(shè)備安裝一次就會(huì)產(chǎn)生一個(gè)ID。假設(shè)每個(gè)鍵值對(duì)占據(jù)32字節(jié),由于一個(gè)個(gè)人云存儲(chǔ)緩存最多可以有128K的大小,那么你最多可以存儲(chǔ)4096個(gè)字段。
在現(xiàn)實(shí)場景中,你的數(shù)據(jù)可能更加復(fù)雜。在這種情況下,存儲(chǔ)的數(shù)據(jù)字段數(shù)也會(huì)進(jìn)一步受到限制。具體而言則需要取決于實(shí)現(xiàn),比如可能需要添加時(shí)間戳來指明每個(gè)字段是何時(shí)修改的。當(dāng)你檢測到有一個(gè)字段在過去幾個(gè)禮拜或者幾個(gè)月的時(shí)間內(nèi)都沒有被修改,那么就可以安全地將它轉(zhuǎn)移到另一個(gè)字段中并刪除老的字段。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
