多人协同编辑算法 —— CRDT 算法 🐂🐂🐂

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777

什么是 CRDT

无冲突复制数据类型(CRDT,Conflict-free Replicated Data Types)是一类在分布式系统中用于数据复制的数据结构,旨在解决多副本并发更新时的数据一致性问题。CRDT 允许各个副本独立且并发地进行更新,而无需协调,且能够在最终自动解决可能出现的不一致性。

CRDT 的关键特性主要有以下三个方面:

  1. 独立更新: 每个副本可以独立地进行更新,无需与其他副本进行通信。

  2. 自动合并: 当副本之间交换数据时,CRDT 会自动合并更新,确保所有副本最终达到一致状态。

  3. 最终一致性: 尽管副本可能在某些时刻处于不同状态,但通过合并操作,所有副本最终会收敛到相同的状态。

CRDT 的种类

CRDT 有两种方法,都可以提供强最终一致性:基于操作的 CRDT 和基于状态的 CRDT。

基于操作的 CRDT(CmRDT)

基于操作的 CRDT(也称为交换性复制数据类型,CmRDT)是一类通过传输更新操作来同步副本的 CRDT。在 CmRDT 中,每个副本只发送更新操作,而不是完整的状态。例如,操作可以是"+10"或"-20",它们表示对某个值的增减。副本接收到这些操作后,会在本地应用这些更新。

操作是可交换的 ,这意味着操作的顺序不影响最终结果。也就是说,即使操作以不同的顺序应用,最终的结果也会是一样的。然而,这些操作不一定是幂等的,即重复应用相同操作可能会产生不同的结果。

由于操作是以独立的方式广播的,通信基础设施必须保证所有操作都被传输到所有副本,而且操作不会丢失或重复。在此过程中,操作的顺序是灵活的,可以按照任何顺序传输。

纯基于操作的 CRDT(Pure CmRDT) 是基于操作的 CRDT 的一个变种,它通过减少所需的元数据大小来优化性能。

G-Counter

G-Counter 用于实现分布式环境中的计数器功能,由多个计数器组成的数据结构,每个副本都维护自己的计数器。每当副本需要增加计数时,它只会在自己的计数器上增加,而不会减少或修改其他副本的计数器。当需要获取计数时,副本会将所有计数器的值累加起来,以获得全局的计数结果。G-Counter 是一个只增长的计数器,它满足如下的性质:

  1. 每个副本的计数器值只增加,不会减少。

  2. 副本之间的计数器值可以独立增长,不会发生冲突或合并操作。

PN-Counter

PN-Counter 是一种基于 CRDT 的数据类型,用于实现分布式环境中的计数器功能。由两个 G-Counter 组成的数据结构,分别用于记录正数和负数的计数。每个副本都维护自己的两个计数器,分别用于增加正数计数和增加负数计数。当需要获取计数时,副本会将正数计数器的值减去负数计数器的值,以获得全局的计数结果。PN-Counter 具有以下性质:

  1. 每个副本的正数计数器和负数计数器值只增加,不会减少。

  2. 副本之间的计数器值可以独立增长,不会发生冲突或合并操作。

  3. 全局计数结果是正数计数减去负数计数的差值。

Sequence CRDT

Sequence CRDT 用于实现分布式环境中的有序序列功能,旨在解决在并发环境中对有序序列进行并发操作时可能出现的冲突问题。它允许并发操作在不同副本之间交换和合并,以达到最终一致性的状态。Sequence CRDT 的实现方式有多种,其中一种常见的实现是基于标识符(Identifier)的方式。每个操作都被赋予唯一的标识符,用于标识操作的顺序。常见的操作包括插入元素、删除元素和移动元素。通过使用标识符和一致的合并策略,Sequence CRDT 可以实现在分布式环境中对有序序列进行并发操作的正确合并。具体的合并策略可以根据应用需求和具体实现进行定制。Sequence CRDT 具有以下特性:

  1. 并发操作可以独立地在不同副本上执行,不会发生冲突。

  2. 合并操作时,可以根据标识符和合并策略将并发操作正确地合并到最终结果中。

Sequence CRDT 可以应用于许多场景,如协同编辑、版本控制系统、聊天应用等,其中有序的操作是必要的。它提供了在分布式环境中实现有序序列的能力,并保持最终一致性。

基于状态的 CRDT(CvRDT)

与基于操作的 CRDT(CmRDT)不同,基于状态的 CRDT(也称为收敛复制数据类型,CvRDT)通过将完整的本地状态发送到其他副本来进行状态传播。在 CvRDT 中,副本接收到完整的状态并将其与自身的状态合并。合并函数必须满足可交换性结合性幂等性,确保副本之间的合并结果是相同的。

这意味着合并操作的顺序不影响最终结果,并且即使多次合并相同的状态,结果也不会发生变化。所有副本的状态都可以通过合并来收敛到同一个最终状态。为了确保一致性,更新函数必须遵循一个偏序规则,使得每次合并都能够单调地增加内部状态。

Delta 状态 CRDT 是基于状态的 CRDT 的一种优化版本。在 Delta CRDT 中,仅传播最近对状态进行的更改(即"delta"),而不是将整个状态传输到其他副本。这减少了每次更新的网络开销,并提高了效率。只有当某个副本的状态发生变化时,才会将该变化广播给其他副本,从而避免了大量不必要的数据传输。

G-Set

G-Set 是一种基于 CRDT 的数据类型,用于实现分布式环境中的集合功能,G-Set 是一个只增长的集合,每个副本都维护自己的本地集合。当需要添加元素时,副本只会在自己的集合中添加元素,而不会删除或修改其他副本的集合。G-Set 的特性包括:

  1. 每个副本维护自己的本地集合,可以独立地增加元素。

  2. 全局集合是所有副本的集合的并集,即包含所有副本中添加的元素。

由于 G-Set 是只增长的集合,它满足最终一致性和合并性质。每个副本的本地集合可以独立地增长,不会发生冲突或合并操作。当需要获取全局集合时,可以简单地将所有副本的集合合并。G-Set 适用于需要在分布式环境中维护集合,并且可以实现高可用性和最终一致性的场景。它常用于记录一组唯一的元素,而不需要删除或修改元素

2P-Set

2P-Set 用于实现分布式环境中的集合功能,维护两个集合:一个"添加集合"和一个"移除集合"。每个元素在添加集合中只能添加一次,在移除集合中只能移除一次。这样,2P-Set 可以实现添加和移除元素的操作,并且确保元素不会重复添加或移除。2P-Set 的操作包括:

  1. 添加元素:将元素添加到添加集合中。

  2. 移除元素:将元素从添加集合中移除,并将其添加到移除集合中。

2P-Set 的特性包括:

  1. 每个副本维护自己的本地添加集合和移除集合,可以独立地进行添加和移除操作。

  2. 全局集合是添加集合减去移除集合的结果。

当需要获取全局集合时,副本将所有副本的添加集合和移除集合合并,并计算添加集合减去移除集合的结果,得到最终的全局集合。2P-Set 可以实现在分布式环境中维护集合,并且具有最终一致性。它适用于需要记录添加和移除操作,并且不希望元素重复添加或移除的场景。

LWW-Element-Set

LWW-Element-Set 用于实现分布式环境中的集合功能,维护一个集合,其中每个元素都与一个时间戳相关联。时间戳可以是递增的整数,逻辑时钟,或其他可比较的时间表示。每当需要添加或移除元素时,副本会将元素与当前时间戳关联,并将操作应用到本地集合。LWW-Element-Set 的特性包括:

  1. 每个副本维护自己的本地集合,可以独立地添加或移除元素。

  2. 全局集合是根据时间戳确定的最新操作的结果,即最后的写操作胜出。

当需要获取全局集合时,副本将所有副本的集合合并,并根据时间戳选择最新的操作。如果存在多个副本对同一个元素进行了不同的操作,那么具有较新时间戳的操作将覆盖较旧时间戳的操作。LWW-Element-Set 可以实现在分布式环境中维护集合,并且具有最终一致性。它适用于需要记录元素的添加和移除,并以最后写操作为准的场景。然而,由于最后写操作胜出的特性,可能会导致某些操作的冲突或覆盖

OR-Set

OR-Set 用于实现分布式环境中的集合功能,维护一个集合,其中每个元素都与一个标识符相关联。当需要添加元素时,副本会为元素生成一个唯一的标识符,并将其添加到本地集合中。当需要移除元素时,副本会为要移除的元素生成一个移除标记,并将其关联到原始元素的标识符上。OR-Set 的特性包括:

  1. 每个副本维护自己的本地集合,可以独立地添加和移除元素。

  2. 全局集合是所有副本的集合的并集,其中移除标记会覆盖对应的元素。

当需要获取全局集合时,副本将所有副本的集合合并,并根据标识符和移除标记进行操作。如果一个元素的标识符存在于集合中,但它的移除标记也存在,则该元素被视为已移除。这样,移除操作具有优先级高于添加操作的效果。OR-Set 可以实现在分布式环境中维护集合,并且具有最终一致性。它适用于需要记录元素的添加和移除,并且允许移除操作覆盖添加操作的场景。

CmRDTs 和 CvRDTs

相比于 CvRDTs,CmRDTs 在副本之间传输操作的协议上有更多要求,但当事务数量相对于内部状态的大小较小时,它们使用的带宽较少。然而,由于 CvRDT 的合并函数是可结合的,与某个副本的状态进行合并会包含该副本的所有先前更新。在减少网络使用和处理拓扑变化方面,使用 Gossip 协议可以很好地传播 CvRDT 状态到其他副本。

CRDT 的数学基础

CRDT 的核心在于其合并操作必须满足一组特定的数学性质,这些性质保证了在分布式系统中数据最终能够达到一致。合并操作(通常表示为 ∨)必须满足以下三个关键性质:

1. 交换律(Commutativity)

合并操作的顺序不影响最终结果:

A \\vee B = B \\vee A

这意味着无论是节点 A 先将数据同步给节点 B,还是节点 B 先将数据同步给节点 A,最终的结果都是一样的。这个性质对于分布式系统特别重要,因为在实际环境中,我们无法保证数据同步的顺序。

示例:

css 复制代码
节点1的状态: {a, b}
节点2的状态: {b, c}
合并结果: {a, b, c}  // 无论是1→2还是2→1的同步顺序,结果都相同

2. 结合律(Associativity)

多个合并操作的顺序不影响最终结果:

(A \\vee B) \\vee C = A \\vee (B \\vee C)

这个性质确保了在有多个节点时,无论按什么顺序进行合并,最终结果都是一致的。这对于大规模分布式系统尤为重要,因为数据同步可能涉及多个节点的链式传递。

示例:

css 复制代码
节点1的状态: {a, b}
节点2的状态: {b, c}
节点3的状态: {c, d}

(节点1 ∨ 节点2) ∨ 节点3 = {a, b, c} ∨ {c, d} = {a, b, c, d}
节点1 ∨ (节点2 ∨ 节点3) = {a, b} ∨ {b, c, d} = {a, b, c, d}

3. 幂等律(Idempotency)

重复合并不会改变结果:

A \\vee A = A

这个性质保证了即使同一个更新被应用多次(例如由于网络问题导致的重复传输),也不会影响最终状态。这对于构建容错的分布式系统至关重要。

示例:

css 复制代码
状态A: {a, b, c}
A ∨ A = {a, b, c}  // 重复合并不会产生新的结果

实际意义

这些数学性质的重要性体现在:

  1. 网络分区容忍: 由于交换律和结合律的存在,系统可以在网络分区的情况下继续工作,当连接恢复后可以正确合并数据。

  2. 最终一致性保证: 这些性质确保了无论数据同步的顺序如何,所有副本最终都会收敛到相同的状态。

  3. 去中心化: 不需要中央协调器来处理并发更新,每个节点都可以独立处理更新并最终达到一致。

  4. 容错性: 幂等性确保了系统能够优雅地处理重复的消息,这在不可靠的网络环境中特别重要。

在实际应用中,这些性质使得 CRDT 特别适合构建:

  • 协同编辑系统
  • 分布式数据库
  • 多设备数据同步
  • 离线优先的应用

通过确保这些数学性质,CRDT 能够在不需要复杂的协调机制的情况下,保证分布式系统中数据的最终一致性。

CRDT 是如何处理冲突的

下图描述了 Yjs 中处理冲突的算法模型,它是一个支持点对点传输的冲突处理模型。

首先我们先来解释一下图中的元素:

  1. E(1,0):表示用户 1 在节点 A 和 B 之间插入了数据项。

  2. D(0,1):表示用户 0 在节点 B 和 C 之间插入了数据项。

  3. C(0,0):表示用户 0 在节点 A 和 B 之间插入了数据项。

图示的操作顺序:

  1. 用户 0 插入了 C(0,0) 在节点 A 和 B 之间。

  2. 用户 0 在节点 B 和 C 之间插入了 D(0,1)。

  3. 用户 1 插入了 E(1,0) 在节点 A 和 B 之间。

当两个操作发生并发冲突(例如 C(0,0) 和 E(1,0) 都涉及节点 A 和 B 之间的插入),Yjs 会基于操作的用户标识符来决定哪一个操作先应用。

在这个例子中,用户 1 的标识符(1)大于用户 0 的标识符(0),因此生成的最终文档顺序是 A C D E B

CRDT 机制能够避免传统操作转发(OT)所面临的冲突问题,同时保证最终一致性,原因在于其设计采用了冲突自由的合并规则,而不依赖于复杂的操作变换和中央协调。

在 OT 中,当多个用户并发地对同一数据进行操作时,系统需要通过操作转发和变换来确保操作顺序的一致性。这通常涉及复杂的变换逻辑,例如在两个用户同时修改相同数据位置时,OT 会通过变换算法调整其中一个操作的位置或内容,以确保最终结果一致。尽管 OT 可以解决许多并发冲突,但这种变换机制本身具有高复杂性,特别是在多个用户同时进行操作时,操作的变换和冲突解决可能导致性能瓶颈、维护困难,以及在极端情况下可能产生不一致的结果。

与此不同,CRDT 通过设计内建的合并规则来避免这些问题。每个 CRDT 数据结构都确保其操作是幂等、交换性强且结合性好的,这意味着无论操作顺序如何或是否发生并发操作,所有副本都能够自动且无冲突地合并,最终收敛到一致的状态。CRDT 不依赖于操作的顺序或中央协调,而是依靠每个操作的唯一标识符和局部合并规则来直接解决并发冲突,从而显著减少了在处理冲突时的计算复杂度。

此外,CRDT 的这一机制使得它天然适合高可用性和容错性要求较高的分布式系统,在面对网络分区、节点故障等场景时,系统依然能够继续操作并保证数据一致性。因此,CRDT 更加简洁、易于扩展,并能够在没有显式操作转发和变换的情况下,确保最终一致性,从根本上避免了 OT 中因操作顺序和变换导致的复杂性和潜在冲突。

CRDT 如何解决脏路径问题

在分布式系统中,脏路径(Dirty Path)问题通常出现在多个副本之间进行并发操作时,导致副本之间的数据状态不一致。由于不同副本的操作可能由于网络延迟、分区或同步问题而不同步,这使得系统中可能出现不一致的数据状态。传统的分布式系统通常依赖中心化的协调机制来同步数据,但这也容易引发性能瓶颈和复杂的冲突解决问题。CRDT(冲突自由复制数据类型)通过去中心化和无冲突的操作设计,避免了脏路径问题,确保多个副本能够在并发操作后最终收敛到一致的状态。

以下是 CRDT 如何通过一系列设计原则来解决脏路径问题的详细过程:

1. 唯一标识符与操作标记

CRDT 使用唯一标识符来区分每个操作,每个操作的标识符通常由 用户标识符 (例如用户 ID)和 操作序列号(通常是时间戳或递增的操作编号)组成。唯一标识符保证了操作的顺序,即使这些操作在不同副本上并发发生。

操作标识符的作用:

  • 用户标识符 (例如 AB):确保每个用户的操作是唯一的,防止不同用户的操作发生混淆。
  • 操作序列号 (例如 01):确保同一用户的操作能够按序列号区分,确定操作的顺序。

这种设计避免了因操作没有明确顺序而产生的不一致或冲突,从而有效地避免了脏路径问题。

示例:

假设用户 A 在副本 1 上插入了一个字符 X,操作标识符为 A,0。与此同时,用户 B 在副本 2 上插入了字符 Y,操作标识符为 B,0。每个操作都带有唯一标识符,确保它们在后续合并时能够正确排序。

2. 并发操作的解决

在 CRDT 中,每个副本都能够独立进行操作,当多个副本发生并发操作时,CRDT 使用设计的 合并规则 来自动解决冲突,确保所有副本最终达到一致状态。

如何处理并发操作?

  • 当用户 A 在副本 1 上插入字符 X,并且用户 B 在副本 2 上插入字符 Y 时,两个操作会先在本地副本上执行,然后通过网络传播到其他副本。
  • CRDT 会通过 操作标识符 比较来确定哪个操作先执行。比如,比较 A,0B,0,标识符较小的操作会先应用。

例如,假设 A,0 小于 B,0,那么操作会按顺序执行,首先在副本 1 上插入 X,然后在副本 2 上插入 Y

3. 合并规则与最终一致性

CRDT 的设计关键在于 合并规则,即如何将并发操作合并为一致的状态。这些合并规则确保了即使副本之间的操作顺序不同,最终副本的数据会收敛到相同的状态。

合并规则保证一致性:

  1. 幂等性(Idempotence):一个操作可以多次应用,结果不会改变。如果某个操作被传送到一个副本多次,只会影响一次,避免重复操作带来的问题。
  2. 交换性(Commutativity):操作的顺序不影响最终结果。不同副本的操作可能发生顺序不同,但最终合并时,所有副本的数据状态将是一致的。
  3. 结合性(Associativity):多个操作的合并顺序不影响结果。即使合并操作的顺序不同,最终的合并结果相同。

这些规则使得 CRDT 在面对并发更新时,能够自动解决冲突并收敛到一致的状态。

举例说明:

假设两个用户并发进行插入操作,用户 A 在副本 1 中插入 X,而用户 B 在副本 2 中插入 Y。无论这两个操作的顺序如何,CRDT 会根据合并规则确定最终的顺序,并保证合并后的状态一致。即使两个副本的操作顺序不同,最终的结果将是文本 "XY""YX"(具体顺序依赖于标识符的比较)。

4. 双向链表在 CRDT 中的应用

在一些 CRDT 应用(例如文本编辑器)中,双向链表 被用来存储数据。双向链表的结构非常适合表示具有顺序关系的数据,并且支持高效的插入、删除和更新操作。

双向链表如何解决脏路径问题:

  • 插入和删除操作:当用户在文本中插入或删除字符时,CRDT 会将这些操作表示为双向链表的节点。每个节点都包含指向前一个和后一个节点的指针,使得操作能够在链表的任意位置进行。
  • 并发操作:当多个用户在不同副本上同时修改文本时,CRDT 会根据操作的唯一标识符(例如标识符的大小)来决定操作的顺序。例如,用户 A 在某位置插入字符,用户 B 在另一个位置插入字符,CRDT 会通过合并规则确保这两个操作按正确顺序合并,并更新链表。

通过这种方式,CRDT 可以处理并发插入、删除操作,避免因操作顺序不同而引发脏路径问题。

5. 最终一致性

CRDT 通过合并规则确保所有副本最终一致。即使操作在不同副本之间发生延迟或顺序不同,最终的合并结果会保证一致性。

如何确保最终一致性?

  • 去中心化:CRDT 不依赖中心化的协调,所有副本都能独立执行操作并进行合并。每个副本都维护自己的操作历史,并通过合并规则来自动解决冲突。
  • 同步与传播:每个副本定期与其他副本同步,传播其操作。即使某些副本的更新稍有延迟,最终每个副本的状态都会通过合并收敛到一致。

通过最终一致性,CRDT 确保即使在网络分区或节点故障的情况下,系统中的所有副本最终都会收敛到相同的数据状态,避免了脏路径问题。

6. 避免脏路径:总结

CRDT 解决脏路径问题的关键在于:

  1. 唯一标识符:每个操作都有唯一标识符,确保并发操作能够按照正确顺序合并。
  2. 去中心化合并:CRDT 不依赖中心节点,而是通过去中心化的方式进行合并,每个副本根据合并规则独立解决冲突。
  3. 合并规则的设计:CRDT 使用幂等性、交换性和结合性保证操作的合并无冲突,确保最终一致性。
  4. 双向链表:在存储数据时,双向链表能够高效支持插入和删除操作,并保证操作的顺序正确,同时避免复杂的全局排序。
  5. 最终一致性:CRDT 确保每个副本最终一致,不论操作顺序如何,最终所有副本都会达成一致,避免了因不同步或操作冲突带来的脏路径问题。

通过这些机制,CRDT 确保了分布式系统中的高可用性、容错性和一致性,避免了脏路径问题的出现,并且简化了分布式系统中并发操作的管理。

CRDT 如何解决 UNDO/REDO 问题

在分布式系统中,UNDOREDO 是常见的操作需求,尤其是在分布式应用(如分布式文本编辑器、协作平台等)中,这些操作通常需要确保数据的一致性和正确的操作回溯。然而,传统的事务日志和操作转发(OT)机制在处理这些操作时可能会遇到同步、顺序和冲突等问题。而 CRDT (冲突自由复制数据类型)通过其特有的设计原则,能够优雅地解决 UNDOREDO 问题,保证分布式系统中操作的回滚与重做能够在多个副本间一致地执行。

什么是 UNDOREDO

  • UNDO:是撤销上一步操作的功能,即恢复到操作前的状态。在分布式系统中,UNDO 通常需要回滚到某个特定的历史状态。
  • REDO:是重新执行撤销操作后的功能,将上一步撤销的操作重新应用于数据中。

在分布式系统中,UNDOREDO 需要跨多个副本同步,以保证每个副本中的历史操作可以一致地回滚或重做。此过程可能会受到以下问题的影响:

  • 并发冲突:不同副本上的操作顺序不同,可能会导致状态不一致,尤其是在操作顺序发生变化时。
  • 操作历史的同步:在没有中心化控制的情况下,操作历史的同步可能会变得非常复杂。
  • 最终一致性:确保在分布式环境中,UNDO 和 REDO 不会导致不同副本的数据不一致。

CRDT 如何解决 UNDOREDO 问题

CRDT 提供了一些特性,使其特别适合解决 UNDOREDO 问题,尤其是在分布式环境下。这些特性包括 冲突自由的操作合并幂等性交换性结合性 、以及 最终一致性。通过这些特性,CRDT 可以处理操作回滚和重做时遇到的挑战。

1. 操作历史与逆向操作(Undo/Redo)

CRDT 中的每个操作(如插入、删除等)都有一个唯一标识符。通过设计合适的操作历史结构,CRDT 可以存储每个操作,并支持操作的回溯和重做。这对于分布式系统中的 UNDOREDO 操作至关重要。

操作的存储和标识:

  • 每个操作都有唯一标识符,通常由操作的用户 ID 和时间戳(或操作序列号)组成。
  • CRDT 通常维护一个操作的日志或历史记录,其中记录了所有历史操作以及它们的操作标识符。由于 CRDT 的操作是幂等的(即重复执行不改变结果),因此可以安全地记录和回滚这些操作。

操作回滚(UNDO):

  • UNDO 操作需要逆向地应用上一个操作。例如,如果用户插入了一个字符,UNDO 就需要撤销该插入操作。
  • 在 CRDT 中,通过 逆向操作 来回滚数据。例如,如果插入操作是通过一个 增量计数器 (例如 PN-Counter)进行的,UNDO 操作会通过逆向操作递减计数器的值,从而撤销上一步的插入。

操作重做(REDO):

  • REDO 操作需要将之前撤销的操作重新应用。例如,如果用户撤销了插入字符 X,则 REDO 会重新执行插入字符 X 的操作。
  • 在 CRDT 中,REDO 操作是重新应用已撤销的操作。这些操作会根据其标识符再次插入或删除数据,并通过合并规则确保最终一致性。

2. 如何支持并发和冲突解决

在分布式系统中,UNDOREDO 操作通常是在多个副本之间执行的,可能会遇到并发冲突的问题。CRDT 的核心特性能够有效地解决并发冲突问题,从而确保 UNDOREDO 操作的一致性。

幂等性、交换性和结合性:

  • 幂等性 :确保同一个操作多次应用不会改变最终的结果。例如,当执行 UNDO 时,即使该操作多次传递给不同副本,它的效果仍然是相同的。
  • 交换性 :多个操作的顺序不会影响最终结果。即使在不同副本上执行 UNDOREDO 操作,操作的顺序不会影响最终的数据一致性。
  • 结合性 :多个 UNDOREDO 操作的顺序不影响结果。无论如何组合多个操作,系统最终会达到一致状态。

这些特性使得 CRDT 在多个副本上执行 UNDOREDO 操作时,可以自动解决并发冲突,确保不同副本的数据始终一致。

解决并发冲突的方式:

  • 当多个用户在不同副本上进行并发操作(如同时执行插入、删除或撤销操作)时,CRDT 会根据每个操作的标识符(例如时间戳、序列号等)来确定它们的顺序。
  • 即使副本之间的操作顺序不同,CRDT 通过标识符确保每个操作按正确的顺序合并,从而保证 UNDOREDO 操作能够正确地同步到所有副本。

示例:

  • 假设两个用户 A 和 B 同时进行文本插入操作,在某个时刻用户 A 撤销了插入操作,而用户 B 在该位置再次插入了文本。CRDT 会根据操作的标识符来判断用户 A 的撤销操作和用户 B 的插入操作的执行顺序,保证最终文本的一致性。

3. 最终一致性与操作回溯

CRDT 的设计目标之一是 最终一致性 。即使操作的执行顺序不同,所有副本最终都会达到一致的状态。对于 UNDOREDO 操作,CRDT 确保它们的执行不会破坏最终一致性。

确保一致性:

  • 合并操作 :CRDT 保证所有副本都会根据合并规则最终收敛到一致的状态。无论是 UNDO 还是 REDO 操作,系统通过合并规则将操作结果应用到每个副本,最终所有副本的数据会一致。
  • 最终一致性 :操作的回溯(如 UNDOREDO)不会导致系统中的副本进入不一致的状态,因为每个副本都独立地执行操作并与其他副本同步,最终收敛到一致的数据状态。

4. 双向链表的应用

在一些具体的 CRDT 实现中(例如分布式文本编辑器),使用 双向链表 来存储数据,这使得 UNDOREDO 操作更容易实现。

双向链表支持操作回溯:

  • 每个节点表示一个操作或数据项,操作的顺序通过前驱和后继指针进行连接。通过这个数据结构,撤销(UNDO)和重做(REDO)可以通过更新链表的指针来高效实现。
  • UNDO:通过回退指针来撤销最近的操作,将前驱指针指向当前操作的前一个节点。
  • REDO:通过更新指针来重做撤销的操作,恢复后继指向已撤销操作的下一个节点。

双向链表使得撤销和重做操作在数据结构中非常高效,并且能够根据唯一标识符和合并规则来正确解决冲突。

CRDT 通过以下几个关键特性解决了 UNDOREDO 问题:

  1. 唯一标识符:每个操作有唯一标识符,确保回溯和重做时能按正确顺序执行。
  2. 幂等性、交换性和结合性 :这些特性确保了 UNDOREDO 操作的正确性,并且即使在并发操作的情况下,也能够保证一致性。
  3. 去中心化合并 :每个副本独立处理 UNDOREDO,并通过合并规则确保最终一致性。
  4. 双向链表 :为 UNDOREDO 提供高效的操作存储和回溯机制,特别适合用于文本编辑等场景。

通过这些机制,CRDT 在分布式环境下不仅保证了 UNDOREDO 的一致性,还有效解决了并发冲突和操作历史同步的问题。

CRDT 解决并发冲突

接下来我们将以图片设置 align 属性为例介绍,首先看看 CRDT 如何描述对象属性及属性修改:

左边是图片数据模型,右边是模拟 CRDT 对应的数据结构,图片对象中的每一个字段都使用结构对象去描述内容及内容的修改,这里以 align 字段的代表看它的表达

操作 1️⃣:

左边是图片数据模型,右边是模拟 CRDT 对应的数据结构,图片对象中的每一个字段都使用结构对象去描述内容及内容的修改,这里以 align 字段的代表看它的表达

图中最上方的蓝色结构表示 align 属性的初始值为 "center",其对应的数据结构标识为 (140,20),表示该值由某个用户在某个时刻的操作产生。

随后,用户执行了操作 ①,将 align"center" 修改为 "left",从而生成了一个新的结构对象(图中橙色部分),其标识符为 (141,0)。这个新对象通过 left 指针指向其前一个状态 (140,20),表示该修改是基于 "center" 状态进行的。此时,Map 中的 align 字段被更新为指向这个新的对象。

⚠️ 值得注意的是:此结构中的 leftright 同时承担了两个不同含义------

  • 一方面,它们是链表结构中的指针字段,用于描述节点之间的连接关系;
  • 另一方面,align 的属性值也恰好是 "left""center""right" 之一。

为避免混淆,请理解:结构对象中间的那一块,才是真正表示属性值的内容,而两侧的 left / right 是链表的结构指针。比如在该示例中,中间的 "left" 是修改后的 align 值,而左侧的 left 指针连接了前一个状态 (140,20)

操作 2️⃣:

当然!以下是你后续"并发修改"部分的润色版本,紧接在"顺序修改"之后,风格统一,逻辑清晰,读起来也更专业:

与前面的顺序修改不同,在并发场景中,多个用户几乎同时基于相同的状态进行修改操作。此时,CRDT 会采用特定的合并策略来决定各个操作的插入顺序,从而确保所有副本最终达成一致。

如图所示,这一次有两个用户同时基于状态 (140,20)(即 align = "center")分别执行了修改操作:

  • 用户 A 将 align 改为 "left",生成结构对象 (141,0)

  • 用户 B 将 align 改为 "right",生成结构对象 (142,0)

由于这两个操作是并发的 ,它们都指向相同的前置节点 (140,20),即具有相同的"前提条件"。此时系统将根据每个操作的唯一标识符进行排序合并------在本例中,(141,0) 的优先级低于 (142,0),因此 "left" 会先插入链表,紧接着是 "right"

最终形成的双向链表结构如下:

scss 复制代码
center → left → right
         ↑      ↑
     (141,0) (142,0)

系统将 align 字段指向链表尾部的最新节点 (142,0),因此最终的属性值为 "right"

这种机制展现了 CRDT 在面对并发修改时的优势:无需冲突检测,也不丢失任一修改历史,并能通过一致的排序规则达成最终一致性。

下面看看两个用户并发的执行属性修改时产生的数据结构:

与前面的顺序操作不同,此处执行的操作 ① 和操作 ② 是并发修改 :它们都是基于同一个前置状态,即 align = "center"(标识符为 (140,20))所发起的修改操作。

具体来说:

  • 操作 ① 将 align 修改为 "left",生成新结构对象 (141,0)

  • 操作 ② 将 align 修改为 "right",生成新结构对象 (142,0)

由于两个修改操作的基础状态相同,因此构成并发。在这种情况下,CRDT 会根据标识符的全局有序性来进行合并处理。

在本示例中,(141,0) 的标识符小于 (142,0),因此系统会按照如下顺序进行集成:

  1. 先插入 "left"(操作 ①)

  2. 再插入 "right"(操作 ②)

最终形成如下链表结构:

scss 复制代码
center → left → right
          ↑       ↑
      (141,0)  (142,0)

因此,最终 align 的属性值为 "right",即指向最新插入的节点 (142,0)

这一过程体现了 CRDT 对并发操作的自动合并能力:无需人工干预或冲突解决策略,仅通过标识符排序,就能实现一致性和可预期的合并结果。

顺序修改 vs 并发修改:对比总结

项目 顺序修改 并发修改
操作基础状态 每次操作都基于最新状态 多个操作基于相同的旧状态并发发生
是否存在冲突 无冲突,顺序明确 存在潜在冲突,需合并处理
合并方式 顺序串接,每个结构对象引用上一个 利用标识符排序合并,构建多分支链表结构
是否保留全部修改 ✅ 是,保留完整历史 ✅ 是,所有并发修改都会被表达
最终结果决定方式 最后一个操作决定当前值 标识符最大的修改赢得当前值归属
示例回顾 "center""left""right" "center"["left", "right"],最终为 "right"

在 CRDT 模型下,无论是顺序修改还是并发修改,都能通过结构化的数据表示 + 有序标识符来安全地整合操作,确保最终状态一致,并完整保留修改轨迹。这正是 CRDT 在协同编辑、离线同步等场景下强大而可靠的基础。

参考文章

总结

CRDT(无冲突复制数据类型)是一类用于分布式系统中的数据结构,它通过内建的幂等性、交换性和结合性操作,支持各副本在无协调情况下独立更新并自动合并,最终收敛为一致状态。它避免了传统并发控制中对冲突的显式处理,适用于离线编辑、多端同步、协同操作等高可用场景。通过唯一标识符和结构化合并策略,CRDT 能在面对并发修改、网络分区等挑战时保持数据一致性和操作完整性。

相关推荐
振鹏Dong42 分钟前
超大规模数据场景(思路)——面试高频算法题目
算法·面试
uhakadotcom42 分钟前
Python 与 ClickHouse Connect 集成:基础知识和实践
算法·面试·github
uhakadotcom43 分钟前
Python 量化计算入门:基础库和实用案例
后端·算法·面试
写代码的小王吧1 小时前
【安全】Web渗透测试(全流程)_渗透测试学习流程图
linux·前端·网络·学习·安全·网络安全·ssh
uhakadotcom1 小时前
使用Python获取Google Trends数据:2025年详细指南
后端·面试·github
uhakadotcom1 小时前
使用 Python 与 Google Cloud Bigtable 进行交互
后端·面试·github
uhakadotcom1 小时前
使用 Python 与 BigQuery 进行交互:基础知识与实践
算法·面试
uhakadotcom1 小时前
使用 Hadoop MapReduce 和 Bigtable 进行单词统计
算法·面试·github
小小小小宇1 小时前
CSS 渐变色
前端
snow@li2 小时前
前端:开源软件镜像站 / 清华大学开源软件镜像站 / 阿里云 / 网易 / 搜狐
前端·开源软件镜像站