大约8年前,我为一款应用程序构建了这样的系统,并且我可以分享一些随着应用使用而发展的方法。
我开始将来自任何设备的每个更改(插入、更新或删除)记录到一个“历史记录”表中。因此,例如,如果某人在“联系人”表中更改其电话号码,则系统将编辑contact.phone字段,并添加一个带有action=update、table=contact、field=phone、record=[contact ID]、value=[new phone number]的历史记录。然后,每当设备同步时,它会下载自上次同步以来的历史记录项,并将其应用于其本地数据库。这听起来像上述的“事务复制”模式。
一个问题是在可能在不同设备上创建项目时保持ID唯一。当我开始时,我不知道UUID,所以我使用自动递增的ID并编写了一些复杂的代码在中央服务器上运行,以检查从设备上传的新ID,如果存在冲突则将其更改为唯一ID,并告诉源设备将其更改为其本地数据库中的ID。仅更改新记录的ID并不那么糟糕,但是如果我在联系人表中创建了一个新项目,然后在事件表中创建一个新的相关项目,现在我也需要检查和更新外键。
最终我学会了使用UUID可以避免这个问题,但那时我的数据库已经很大了,我担心完全实施UUID会导致性能问题。因此,我开始使用随机生成的8个字符的字母数字键作为ID,而不是使用完整的UUID,并留下了现有代码来处理冲突。在我的当前8个字符键和UUID的36个字符之间,必须有一个最佳选择,既可消除冲突又不会增加不必要的负担,但由于我已经有了解决冲突的代码,所以尝试寻找最佳选择并不是优先考虑的事情。
接下来的问题是历史记录表的大小约为整个数据库的10倍。这使得存储变得昂贵,并且对历史记录表进行任何维护都可能很痛苦。保留整个表可以让用户回滚到以前的任何更改,但这开始感觉像过度设计。因此,我添加了一个例行程序,在同步过程中,如果设备最后下载的历史记录项不存在于历史记录表中,服务器将不会提供最近的历史记录项,而是提供一个包含该账户所有数据的文件。然后,我添加了一个cronjob,用于删除90天以上的历史记录项。这意味着用户仍然可以回滚到90天以内的更改,并且如果他们每90天至少同步一次,更新将像以前一样增量进行。但是,如果超过90天没有同步,应用程序将替换整个数据库。
这种变更将历史表的大小减少了近90%,因此现在维护历史表只会使数据库变成原来的两倍,而不是十倍。该系统的另一个好处是,即使需要,同步仍然可以在没有历史记录表的情况下工作 - 就像我需要进行一些离线维护一样。或者我可以为不同价格点的帐户提供不同的回滚时间段。如果有超过90天的更改需要下载,则完整文件通常比增量格式更高效。
如果今天我从头开始,我会跳过ID冲突检查,并努力实现足以消除冲突的关键长度,并进行某种错误检查以防万一。(看起来YouTube使用11个字符的随机ID。)历史表和最近更新的增量下载组合,或在需要时进行完整下载一直运行良好。