在线/离线数据管理

Posted

技术标签:

【中文标题】在线/离线数据管理【英文标题】:online/offline data management 【发布时间】:2015-06-28 16:56:05 【问题描述】:

我必须创建一个功能类似于联系人应用程序的应用程序。您可以在客户的 iPhone 上添加联系人,它应该会上传到客户的 iPad。如果客户在他们的 iPad 上更新联系人,它应该在他们的 iPhone 上得到更新。

其中大部分都是相当直接的。我使用Parse.com 作为我的后端并使用Core Data 在本地保存联系人。我遇到的唯一问题是在用户离线时管理联系人。

假设我有一部 iPhone 和一部 iPad。他们俩目前都拥有相同版本的在线数据库。我的 iPhone 现在离线了。现在是上午 9 点。

上午 10 点,我在 iPad 上更新联系人的电话号码。它在本地和在线保存更改。上午 11 点,我在 iPhone 上更新了同一联系人的电子邮件地址,但仍处于离线状态。

中午,我的 iPhone 连接到互联网并检查服务器是否有更改。它发现其更改比最新更新更新(检查updatedAt 时间戳属性),因此它不会下载联系人的新电话号码(“已过时”),而是覆盖电话号码以及电子邮件地址(将新电话号码更新为旧版本,因为它在上午 10 点更新电话号码期间处于离线状态,并且其更改应该是最近的)。

我应该如何处理上面遇到的在线/离线问题?我能想到的一个解决方案是在联系人的每个属性上保持更新的时间戳,而不仅仅是整个联系人的通用 updatedAt 属性,例如什么时候更新名字,什么时候更新姓氏,然后手动检查离线设备是否对每个属性都有更新,而不是覆盖整个对象,但这似乎很草率。

我还考虑在每个 Core Data 对象上设置一个 updatedLocallyupdatedOnline 时间戳属性。这样,如果两者不匹配,我可以进行差异检查并使用最新的冲突来解决,但这似乎仍然不是最干净的解决方案。有没有其他人遇到过类似的事情?如果有,你是怎么解决的?

我认为的伪代码/摘要?涵盖了所有测试用例,但仍然不是很优雅/完整:

Parse.com 上的 2 个实体:联系方式和联系历史记录

联系人有first、last、phone、email、onlineUpdate

联系人历史记录具有要引用的联系人的主键和相同的属性,但具有历史记录。例如first: [value:"josue",onlineUpdate:"9AM",value:"j",onlineUpdate:"10AM",value:"JOSUEESP",onlineUpdate:"11AM"]

Core Data 上的 1 个实体,联系人:

Contact 有第一部、最后一部电话、电子邮件、onlineUpdate 和offlineUpdate(重要提示:这仅在 Core Data 上,不在 Parse 上)

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:您应该如何构建一种用于在线/离线更新而不会意外覆盖的版本控制系统?我想将带宽使用限制到最低限度。

【问题讨论】:

有一个类似的问题,并用你的第一个解决方案解决了这个问题,每个键都有一个 updatedAt 字段。当然不是很干净,但似乎是防止意外覆盖的最安全的解决方案...... 有一个适用于 ios 的 Parse SDK,它包含离线缓存。为什么不使用它并让 Parse 以专有方式处理时间戳? 我在这里进行了调查:***.com/questions/31091258/…,但这似乎不是我的解决方案。如果您能想出一种仅使用 Parse 来实现我需要的方法,我很想听听! 在这种情况下,也许将 Parse 与 Realm 结合起来? realm.io 至少可以减轻将对象转换为 JSON 的负担 为什么要将我的数据转换为JSON? Parse 提供了一个易于操作的PFObject 【参考方案1】:

我建议使用基于密钥的更新而不是基于联系人的更新。 您不应该将整个联系人发送到服务器,在大多数情况下,用户无论如何都会更改一些属性(诸如“姓氏”之类的东西通常不会经常更改)。这也减少了带宽使用。 随着离线联系人的应用更改,您发送 您本地联系人与服务器的旧版本号/上次更新时间戳。服务器现在可以 只需查看您的旧版本号,即可确定您的本地数据是否是最新的。 如果您的旧版本号与服务器的当前版本号匹配,则您的客户端无需更新任何其他信息。如果不是这种情况,服务器应向您发送新联系人(在应用您请求的更新后)。 您还可以保存这些提交,这将导致联系历史记录 它不会在每次更改密钥时存储整个联系人,而只会存储更改本身。 伪代码中的简单实现可能如下所示:

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 AM 客户端和服务器都是最新的,每台设备都在线

每个设备都有一个用于联系人“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 连接到网络并且没有要提交的更改时,它应该只发送当前版本的联系人并查看它是否仍然是最新的。


现在您已经提交了两个离线更改,而没有相互覆盖。 您可以轻松扩展此方法并进行一些优化。 例如:

如果客户尝试更新旧版本的联系人,请不要将整个联系人作为答案发送给他们。而是将他们错过的提交发送给他们,让他们自己更新他们的联系方式。如果您存储大量有关您的客户端的信息并期望在更新之间进行少量更改,这将非常有用。 如果客户更新了有关联系人的所有信息,我们可以假设他不需要知道错过的更新,但是我们会让他知道他错过的所有信息(但这对他/应该没有影响)李>

我希望这有帮助。

【讨论】:

【参考方案2】:

我对 iO、核心数据和 parse.com 一无所知,所以我只能建议一个通用的算法解决方案。我认为您可以采用类似于版本控制系统中所做的方法。

最简单的方法是将所有历史记录保存在服务器上:保存联系人列表的所有修订。现在,在同步期间,手机会发送有关它所看到的最后一个服务器版本的信息,并且该版本将是当前手机版本和当前服务器版本的“共同父”。

现在您可以看到自该修订以来服务器和手机上发生了什么变化,并应用通常的 3 向比较:如果某些字段仅在服务器上发生变化,则将新字段发送到手机;如果某些字段仅在手机上更改,则在服务器上也进行更改,如果某些字段在手机和服务器上都已更改并且更改不同,则您有冲突,必须询问用户。

这种方法的一种变体可能是使用更改,而不是修订。服务器和客户端的主要数据都不是联系人列表,而是其更改的历史记录。 (如果需要,也可以保留当前的联系人列表以及一组“关键帧”;它不会用于冲突解决算法,但可以用于快速显示和使用。)

然后,当用户同步数据时,您只下载/上传更改。如果有任何冲突更改,您只能询问用户,否则您只需合并它们。如何定义更改以及哪些更改被认为是冲突的,取决于您。一个简单的方法可以将更改定义为一对(字段,新值),如果两个更改具有相同的字段,则它们是冲突的。您还可以使用更高级的冲突解决逻辑,例如,如果一个更改仅更改了电子邮件的前半部分,而另一个更改更改了后半部分,那么您可以合并它们。

【讨论】:

【参考方案3】:

执行此操作的正确方法是保留事务日志。每当您保存在 Core Data 中时,您都会在事务日志中创建一个日志条目。当您下次在线时,您可以针对服务器回放事务日志。

这就是 iCloud 和其他同步服务的工作方式。

【讨论】:

总的来说,我认为你是对的,但你在回答中遗漏了很多细节,比如它应该如何处理不同的事务日志以及它们之间的时间安排。 99% 的细节是商业决策。【参考方案4】:

您可以使用单独的表来存储所有更新的联系人的 ID(存储联系人信息的数据库表中的主键),而不是为每个核心数据对象设置单独的标志。

李>

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

【讨论】:

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

以上是关于在线/离线数据管理的主要内容,如果未能解决你的问题,请参考以下文章

如何在线保存/加载数据(使用 AJAX 和 JSON 存储数据)和离线(本地)

推荐一款 在线+离线数据 同步框架 Dotmim.Sync

如何创建 Flutter 应用的离线和在线

离线/在线数据同步策略

Redis技术探索「数据迁移实战」手把手教你如何实现在线+离线模式进行迁移Redis数据实战指南(在线同步数据)

是否可以将数据从离线网站传输到在线数据库? [关闭]