我们有两个通常无法相互通信的离线系统。两个系统都保持相同的有序项目列表。他们很少能够相互通信以同步列表。
项目标有修改时间戳以检测编辑。项目由UUID标识,以避免在插入新项目时发生冲突(与使用自动递增整数相反)。检测到同步新UUID并将其复制到另一个系统时。删除也是如此。
上述数据结构适用于无序列表,但我们如何处理排序?如果我们添加整数“rank”,那么在插入新项目时需要重新编号(因此需要同步所有后继项目,因为只有1次插入)。或者,我们可以使用小数排名(使用前一项和后继项的排名的平均值),但这似乎不是一个强大的解决方案,因为当插入许多新项时,它会很快遇到准确性问题。
我们还考虑将其作为双重链接列表实现,每个项目都包含其前任和后续项目的UUID。但是,当插入1个新项目时,仍然需要同步3个项目(或者当删除1个项目时同步其余2个项目)。
优选地,我们想要使用仅需要同步新插入的项的数据结构或算法。这样的数据结构是否存在?
编辑:我们需要能够处理将现有项目移动到其他位置!
答案 0 :(得分:5)
插值排名方法确实没有问题。只需基于可变长度位向量定义您自己的编号系统,该向量表示0到1之间的二进制分数,没有尾随零。二进制点位于第一个数字的左侧。
该系统唯一的不便之处在于空位向量给出的最小可能键为0。因此,只有当您肯定关联项永远是第一个列表元素时才使用它。通常,只需给第一项键1.这相当于1/2,因此范围(0..1)中的随机插入将倾向于最小化位使用。要在之前和之后插入项目,
01 < newly interpolated = 1/4
1
11 < newly interpolated = 3/4
再次插值:
001 < newly interpolated = 1/8
01
011 < newly interpolated = 3/8
1
101 < newly interpolated = 5/8
11
111 < newly interpolated = 7/8
请注意,如果您希望省略存储最终1!所有键(除了0,你通常不会使用)以1结尾,因此存储它是绝对的。
二进制分数的比较很像词汇比较:0 <1并且从左到右扫描的第一位差异告诉您哪个更少。如果没有出现差异,即一个向量是另一个向量的严格前缀,则较短的向量较小。
使用这些规则,可以非常简单地提出一种算法,该算法接受两个位向量并计算它们之间大致(或在某些情况下)的结果。只需添加位串,然后向右移1,删除不必要的尾随位,即取两者的平均值来分割范围。
在上面的例子中,如果删除了我们:
01
111
我们需要插入这些内容,添加01(0)
和111
以获取1.001
,然后转到获取1001
。这作为插值工作正常。但请注意,最终1
不必要地使它比任何一个操作数都长。一个简单的优化是删除最后的1
位以及尾随零以简单地1
。果然,1
差不多是我们希望的一半。
当然,如果在同一位置进行多次插入(例如,在列表的开头插入连续插入),位向量将变长。这与插入二叉树中相同点的现象完全相同。它长得又长又细。要解决此问题,您必须在同步期间通过使用最短的可能位向量重新编号来“重新平衡”,例如:对于14,你可以使用上面的序列。
<强>加成强>
虽然我没有尝试过,但Postgres bit string type似乎足以满足我所描述的键。我需要验证的是整理顺序是正确的。
此外,对于任何k>=2
,相同的推理对于基数为k的数字也可以正常工作。第一项获取密钥k/2
。还有一个简单的优化可以防止在末尾和前面分别附加和预先添加元素的常见情况,从而导致长度为O(n)的键。它为这些情况维护O(log n)(尽管在p插入后在内部插入相同的位置仍然可以产生O(p)键)。我会让你解决这个问题。如果k = 256,则可以使用不定长度的字节字符串。在SQL中,我相信你想要varbinary(max)
。 SQL提供了正确的字典排序顺序。如果你有一个类似于Java的BigInteger
包,那么插值操作的实现很容易。如果您喜欢人类可读的数据,可以将字节字符串转换为例如十六进制字符串(0-9a-f)并存储它们。然后正常的UTF8字符串排序顺序正确。
答案 1 :(得分:2)
您可以为每个项添加两个字段 - “创建时间戳”和“插入后”(包含插入新项目后项目的ID)。同步列表后,发送所有新项目。这些信息足以让您能够在另一方构建列表。
使用收到的新添加项目列表,执行此操作(在接收端):按创建时间戳排序,然后逐个进行,并使用“插入后”字段在适当的位置添加新项目。
如果添加了项目A,则可能会遇到问题,然后在A之后添加B,然后删除A.如果发生这种情况,您还需要同步A(基本上同步自上次同步以来列表中发生的操作,而不仅仅是当前列表的内容)。它基本上是一种日志传送形式。
答案 2 :(得分:1)
你可以看看“镜头”,这是双向编程概念。 例如,您的问题似乎已经解决了this paper中描述的“匹配镜头”。
答案 3 :(得分:1)
我认为这里适合的数据结构是order statistic tree。在统计树的顺序中,您还需要维护子树的大小以及其他数据,大小字段可以根据需要通过排名轻松找到元素。排名,删除,更改位置,插入等所有操作均为O(logn)
。
答案 4 :(得分:1)
我认为你可以在这里尝试一种交易方法。例如,您不是物理删除项目,而是将它们标记为删除,并仅在同步期间提交更改。我不确定您应该选择哪种数据类型,这取决于您希望哪些操作更高效(插入,删除,搜索或迭代)。
让我们在两个系统上都有以下初始状态:
|1| |2|
--- ---
|A| |A|
|B| |B|
|C| |C|
|D| |D|
之后,第一个系统将元素A
标记为删除,第二个系统在BC
和B
之间插入元素C
:
|1 | |2 |
------------ --------------
|A | |A |
|B[deleted]| |B |
|C | |BC[inserted]|
|D | |C |
|D |
两个系统都会考虑到本地更改继续处理,系统1忽略元素B
,系统2将元素BC
视为普通元素。
同步发生时:
据我所知,每个系统都从其他系统接收列表快照,两个系统都会冻结处理,直到完成同步。
因此,每个系统按顺序迭代接收到的快照和本地列表,并在“事务提交”之后将更改写入本地列表(根据修改的时间戳解决可能的冲突),最终应用所有本地更改并删除有关它们的信息。 例如,对于系统一:
|1 pre-sync| |2-SNAPSHOT | |1 result|
------------ -------------- ----------
|A | <the same> |A | |A |
|B[deleted]| <delete B> |B |
<insert BC> |BC[inserted]| |BC |
|C | <same> |C | |C |
|D | <same> |D | |D |
系统唤醒并继续处理。
项目按插入顺序排序,移动可以实现为同时删除和插入。另外我认为有可能不会传输整个列表shapshot而只传输实际修改过的项目列表。
答案 5 :(得分:1)
我认为,广泛地说,Operational Transformation可能与您在此描述的问题有关。例如,考虑实时协作文本编辑的问题。
我们基本上有一个排序的项目(单词)列表,需要保持同步,并且可以在列表中随机添加/修改/删除。我看到的唯一主要区别在于对列表进行修改的周期性。(你说它不经常发生)
运营转型确实是一个充分研究的领域。我可以找到this blog article给出指点和介绍。此外,对于Google Wave所遇到的所有问题,他们实际上在运营转型领域取得了重大进展。检查this out.。关于这个主题有很多文献可供参考。请查看此stackoverflow thread和Differential Synchronisation
令我印象深刻的另一个问题是文本编辑器中使用的数据结构 - Ropes。 因此,如果您有操作日志,可以说,“索引5已删除”,“索引6已修改为ABC”,“索引8已插入”,您现在可能需要做的是从系统A传输更改的日志到系统B,然后在另一侧顺序重建操作。
另一个“实用工程师”的选择是在系统A改变时简单地重建系统B上的整个列表。根据实际频率和变化的大小,这可能没有听起来那么糟糕。
答案 6 :(得分:1)
通过在每个项目上包含PrecedingItemID
(如果项目是有序列表的顶部/根目录,可以为null),我暂时解决了类似的问题,然后拥有一种保持的本地缓存排序顺序中所有项目的列表(这纯粹是为了提高效率 - 所以每次在本地客户端上重新排序时,您不必递归地查询或构建基于PrecedingItemID
的列表) 。然后,当需要同步时,我会执行稍微昂贵的操作,查找两个项目请求相同的PrecedingItemID的情况。在这些情况下,我只是按创建时间排序(或者你想要调和哪一个获胜并先来),将第二个(或其他)放在后面,然后继续订购列表。然后我将这个新订单存储在本地订购缓存中,并继续使用它直到下一次同步(只需确保随时更新PrecedingItemID
)。
我还没有对这种方法进行单元测试 - 所以我并不是100%肯定我没有错过一些有问题的冲突场景 - 但它至少在概念上似乎能够满足我的需求,这听起来与OP的相似