在线/离线数据管理

12
我需要创建一个应用程序,其功能类似于联系人应用程序。您可以在客户的iPhone上添加联系人,并将其上传到客户的iPad上。如果客户在他们的iPad上更新联系人,则应在他们的iPhone上进行更新。大部分都很简单。我正在使用Parse.com作为后端,在本地保存联系人,使用Core Data。唯一遇到的问题是在用户离线时管理联系人。如何处理此类在线/离线问题?我的解决方案是为每个联系人的每个属性保持更新的时间戳,而不仅仅是整个联系人的updatedAt属性,以便在离线设备具有更改时手动检查其是否具有更改。我还考虑在每个Core Data对象上拥有updatedLocallyupdatedOnline时间戳属性。这样,如果两者不匹配,我可以进行差异检查并对冲突使用最近的一个,但这仍然不是最简洁的解决方案。 有其他人遇到类似的情况吗?如果是,你是怎么解决的?
for every contact in parse database as onlineContact {
    if onlineContact does not exist in core data {
        create contact in core data
    }
    else {
        // found matching local object to online object, check for changes
        var localContact = core data contact with same UID as onlineContact
        if localContact.offlineUpdate more recent than onlineContact.onlineUpdate {
            for every attribute in localContact as attribute {
                var lastOnlineValueReceived = Parse database Contact History at the time localContact.onlineUpdate for attribute
                if lastOnlineValueReceived == localContact.attribute {
                    // this attribute did not change in the offline update. use latest available online value
                    localContact.attribute = onlineContact.attribute
                }
                else{
                    // this attribute changed during the more recent offline update, update it online
                    onlineContact.attribute = localContact.attribute
                }
            }
        }
        else if onlineContact.onlineUpdate more recent than localContact.offlineUpdate {
            // another device updated the contact. use the online contact.
            localContact = offlineContact
        }
        else{
            // when a device is connected to the internet, and it saves a contact
            // the offline/online update times are the same
            // therefore contacts should be equivalent in this else statement
            // do nothing
        }
}

TL;DR: 您应该如何构建一种在线/离线更新的版本控制系统,以避免意外覆盖?我想将带宽使用限制到最低。


1
遇到了类似的问题,用你提供的第一个解决方案解决了它,为每个键添加了updatedAt字段。虽然不是很干净,但似乎是防止意外覆盖的最安全的解决方案... - eschanet
有一个适用于iOS的Parse SDK,它包含离线缓存功能。为什么不使用它,让Parse以其专有的方式处理时间戳呢? - Max MacLeod
我在这里进行了调查:http://stackoverflow.com/questions/31091258/using-parse-to-replace-core-data,但似乎对我来说并不是解决方案。如果您能想到一种仅使用Parse实现我所需的功能的方法,我很乐意听取! - Josue Espinosa
在这种情况下,也许可以将Parse与Realm结合使用?https://realm.io 至少它会减轻将您的对象转换为JSON的负担。 - Max MacLeod
为什么要将数据转换为 JSON?解析提供了一个易于操作的 PFObject - Josue Espinosa
4个回答

2
我建议使用基于关键字的更新而不是基于联系人的更新。

通常情况下,您不应将整个联系人发送到服务器,因为用户通常只会更改一些属性(例如“姓氏”通常不会经常更改)。这也可以减少带宽使用。
在应用离线联系人的更改时,您将本地联系人的旧版本号/最后更新时间戳发送到服务器。现在,服务器可以通过查看旧版本号来确定您的本地数据是否是最新的。
如果您的旧版本号与服务器的当前版本号匹配,则无需更新任何其他信息。如果不是这种情况,服务器应向您发送新联系人(在应用您请求的更新之后)。

您还可以保存这些提交,这将导致联系人历史记录不会每次更改关键字时都存储整个联系人,而只存储更改本身。

伪代码中的简单实现如下:
for( each currentContact in offlineContacts ) do
{

if( localChanges.length > 0){      // updates to be made
    commitAllChanges();
    answer = getServerAnswer();

    if(answer.containsContact() == true){  
                                  // server sent us a contact as answer so 
                                  // we should overwrite the contact
    currentContact = answer.contact;
    } else {
      // the server does not want us to overwrite the contact, so we are up to date!
    }
    // ... 

}
} // end of iterating over contacts

服务器端看起来也同样简单:

for (currentContactToUpdate in contactsToUpdate) do 
{   
    sendBackContact = false;   // only send back the updated contact if the client missed updates
    for( each currentUpdate in incomingUpdates ) do {
        oldClientVersion = currentUpdate.oldversion;
        oldServerVersion = currentContact.getVersion();

       if( oldClientVersion != oldServerVersion ){
            sendBackContact = true;
            // the client missed some updates from other devices
            // because he tries to update an old version
       } 

       currentContactToUpdate.apply(currentUpdate);

    }

    if(sendBackContact == true){
       sendBack(currentUpdate);
    }
}

为了更好地理解工作流程,我将提供一个示例:


早上8点,客户端和服务器都是最新的,每个设备都在线。

每个设备都有一个与联系人“Foo Bar”相关的条目(在本例中为一行),其主键ID相同。版本号对于每个条目都是相同的,因此它们全部都是最新的。

 _        Server    iPhone    iPad
 ID       42        42        42 
 Ver      1         1         1
 First    Foo       Foo       Foo
 Last     Bar       Bar       Bar
 Mail     f@b       f@b       f@b

早上9点,你的iPhone离线了。你注意到Foo Bar的电子邮件地址变成了“foo@b”。你可以通过以下步骤在手机上更改联系人信息:

UPDATE 42 FROM 1          TO 2             Mail=foo@b
 //    ^ID     ^old version  ^new version  ^changed attribute(s)

现在,您手机中的联系人将如下所示:
 _        iPhone   
 ID       42       
 Ver      2       
 First    Foo      
 Last     Bar   
 Mail     foo@b   



早上10点,你的iPad离线了。你发现“Foo Bar”实际上应该写成“Voo Bar”!你立即在iPad上应用更改。

UPDATE 42 FROM 1 TO 2 First=Voo

注意,iPad 仍然认为联系人42的当前版本为1。由于没有设备连接到网络,因此服务器和iPad都没有注意到您如何更改邮件地址并增加版本号。这些更改仅在本地存储,并且只能在您的iPad上看到。

上午11点,您将iPad连接到网络。 iPad向服务器发送最新更新。

之前:

 _        Server    iPad
 ID       42        42 
 Ver      1         2
 First    Foo       Voo
 Last     Bar       Bar
 Mail     f@b       f@b


iPad -> 服务器:

UPDATE 42 FROM 1 TO 2 First=Voo

现在服务器可以看到您正在更新联系人42的版本1。由于版本1是当前版本,因此您的客户端已经是最新的(在您离线期间没有进行任何更改)。

服务器 -> iPad

UPDATED 42 FROM 1 TO 2 - OK


之后:

 _        Server    iPad
 ID       42        42 
 Ver      2         2
 First    Voo       Voo
 Last     Bar       Bar
 Mail     f@b       f@b



12点,你将iPad断开网络并连接了iPhone。 iPhone尝试提交最近的更改。

之前:

 _        Server    iPhone
 ID       42        42 
 Ver      2         2
 First    Voo       Voo
 Last     Bar       Bar
 Mail     f@b       foo@b


将iPhone设备连接到服务器

UPDATE 42 FROM 1 TO 2 Mail=foo@b

服务器会察觉到您试图更新同一联系人的旧版本。由于您的更新比iPad的更新更加新,因此服务器将应用您的更新,但会将新的联系人数据发送给您,以确保您也能获得更新后的名字。

 _        Server    iPhone
 ID       42        42 
 Ver      2         2
 First    Voo       Voo
 Last     Bar       Bar
 Mail     foo@b     foo@b


服务器 -> iPad

UPDATED 42 FROM 1 TO 3 - Ver=2;First=Voo;.... // send the whole contact
/* Note how the version number was changed to 3, and not to 2, as requested.
*  If the new version number was (still) 2 the iPad would miss the update
*/

下一次您的iPad连接到网络并且没有更改要提交时,它应该只发送当前版本的联系人,并查看它是否仍然是最新的。
现在您已经提交了两个离线更改,而又没有相互覆盖。您可以轻松扩展此方法并进行一些优化。例如:
- 如果客户端尝试更新旧版本的联系人,则不要将整个联系人作为答案发送给他们。而是向他们发送错过的提交请求,并让他们自己更新联系人。如果您存储有关客户的大量信息并且预计在更新之间完成较少的更改,则此选项很有用。 - 如果客户端更新了有关联系人的所有信息,则我们可以假设他不需要知道错过的更新。但是,我们仍会通知他有关他错过的所有内容(但对他没有影响)。
希望这可以帮助您。

1
我对iOs、core data和parse.com一无所知,因此我只能建议一个通用的算法解决方案。我认为你可以采用类似版本控制系统的方法。
最简单的方法是在服务器上保留所有历史记录: 保留联系人列表的所有修订版本。现在,在同步期间,手机发送有关它所看到的最后一个服务器修订版的信息,这个修订版将是当前手机修订版和当前服务器修订版的“共同父级”。
现在,您可以查看自从那个修订版以来在服务器和手机上发生了什么变化,并应用常规的三向比较: 如果某个字段仅在服务器上更改,则将新字段发送到手机; 如果某个字段仅在手机上更改,则也要在服务器上更改它;如果某个字段在手机和服务器上都被更改且更改不同,则存在冲突并需要询问用户。
一种变化的方法可能是使用“更改”而不是“修订版”。服务器和客户端上的主要数据将不是联系人列表,而是其更改历史记录。(如果需要,当前联系人列表以及一组“关键帧”也可以保留;它不会用于冲突解决算法,但可以快速显示和使用。)
然后,当用户同步数据时,您只需下载/上传更改。如果存在任何冲突更改,则除了询问用户外,您没有其他选择,否则只需合并它们。如何定义更改以及哪些更改被视为冲突取决于您。一个简单的方法可以将更改定义为一对(字段,新值),如果它们具有相同的字段,则两个更改会发生冲突。您还可以采用更高级的冲突解决逻辑,例如如果一个更改仅更改电子邮件的前半部分,而另一个更改后半部分,则可以将它们合并。

0

正确的做法是保留一个事务日志。每当您在Core Data中保存时,都会在事务日志中创建一个日志条目。下次您联机时,可以将事务日志回放到服务器上。

这就是iCloud和其他同步服务的工作原理。


总的来说,我认为你是正确的,但你在回答中忽略了很多细节,比如它应该如何处理不同的事务日志以及它们之间的时间安排。 - AndersK
99%的细节都是商业决策。 - Marcus S. Zarra

-1
  • 不要为每个核心数据对象单独设置标志,可以有一个单独的表格,它将存储所有已更新联系人的ID(来自存储联系信息的数据库表的主键)。

  • 稍后当用户上线时,您只需从实际的联系人详细信息表中获取这些联系人,并将它们上传到您的服务器上。


我不太确定你的意思,答案有点不清楚。你能更具体地说明为什么要这样做吗?这解决了什么问题? - Josue Espinosa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接