第6讲:CRDT 实现

前五讲我们基于 OT 算法构建了协同编辑引擎。OT 虽然强大,但它依赖中央服务器来做操作排序和变换。有没有一种方式,可以让多个客户端在没有服务器的情况下也能自动达成一致

这就是 CRDT(Conflict-free Replicated Data Types)的用武之地。


一、CRDT vs OT

1.1 核心区别

OT 的工作方式:
  客户端 A ──op1──▶ 服务端 ──op1'──▶ 客户端 B
  客户端 B ──op2──▶ 服务端 ──op2'──▶ 客户端 A
  
  需要服务端做变换,保证一致性

CRDT 的工作方式:
  客户端 A ──op1──▶ 客户端 B
  客户端 B ──op2──▶ 客户端 A
  
  无需中心服务端,每个客户端独立合并操作
  数学保证:无论顺序如何,最终结果一致

1.2 对比表

特性

OT

CRDT

中心化

需要中央服务器

去中心化

离线支持

强(天然支持)

算法复杂度

中等

较高

存储开销

较高(需保留元数据)

收敛保证

需要正确实现

数学保证

典型应用

Google Docs

Figma, Atom Teletype

1.3 本讲实现的 CRDT 类型

我们将实现三种 CRDT:

1. LWW-Register (Last-Writer-Wins Register)
   - 最简单的 CRDT
   - 每个值带时间戳,最新的胜出
   - 适用于配置、元数据

2. G-Counter (Grow-only Counter)
   - 只能增加的计数器
   - 每个节点维护自己的计数
   - 合并时取各节点最大值之和

3. RGA (Replicated Growable Array)
   - 可增长的数组(文本的核心)
   - 每个字符带唯一 ID
   - 插入操作天然可交换

二、基础 CRDT 类型

2.1 LWW-Register

# core/crdt/lww_register.py
"""
LWW-Register:最后写入者胜出的寄存器
"""

from __future__ import annotations
from typing import Any, Optional, Tuple
from dataclasses import dataclass
import time
import uuid


@dataclass
class Timestamp:
    """
    时间戳(带节点 ID,保证全局唯一)
    
    比较规则:先比较时间,再比较节点 ID
    """
    wall_clock: float
    node_id: str
    
    def __lt__(self, other: Timestamp) -> bool:
        if self.wall_clock != other.wall_clock:
            return self.wall_clock < other.wall_clock
        return self.node_id < other.node_id
    
    def __le__(self, other: Timestamp) -> bool:
        return self < other or self == other
    
    def __eq__(self, other: Timestamp) -> bool:
        return (self.wall_clock == other.wall_clock and 
                self.node_id == other.node_id)
    
    def to_dict(self) -> dict:
        return {
            'wall_clock': self.wall_clock,
            'node_id': self.node_id
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> Timestamp:
        return cls(
            wall_clock=data['wall_clock'],
            node_id=data['node_id']
        )
    
    @classmethod
    def now(cls, node_id: str) -> Timestamp:
        return cls(wall_clock=time.time(), node_id=node_id)


class LWWRegister:
    """
    LWW-Register
    
    每个副本保存 (值, 时间戳) 对。
    合并时取时间戳最大的值。
    """
    
    def __init__(self, node_id: str, initial_value: Any = None):
        self.node_id = node_id
        self.value = initial_value
        self.timestamp = Timestamp.now(node_id) if initial_value else None
    
    def set(self, value: Any):
        """
        设置值
        
        Args:
            value: 新值
        """
        self.value = value
        self.timestamp = Timestamp.now(self.node_id)
    
    def get(self) -> Any:
        """获取当前值"""
        return self.value
    
    def merge(self, other: LWWRegister):
        """
        合并另一个副本
        
        取时间戳更大的值。
        
        Args:
            other: 另一个副本
        """
        if other.timestamp and self.timestamp:
            if other.timestamp > self.timestamp:
                self.value = other.value
                self.timestamp = other.timestamp
        elif other.timestamp and not self.timestamp:
            self.value = other.value
            self.timestamp = other.timestamp
    
    def to_dict(self) -> dict:
        return {
            'value': self.value,
            'timestamp': self.timestamp.to_dict() if self.timestamp else None,
            'node_id': self.node_id
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> LWWRegister:
        reg = cls(node_id=data['node_id'])
        if data['timestamp']:
            reg.timestamp = Timestamp.from_dict(data['timestamp'])
            reg.value = data['value']
        return reg
    
    def __repr__(self) -> str:
        return f"LWWRegister({self.value} @ {self.timestamp})"

2.2 G-Counter

# core/crdt/gcounter.py
"""
G-Counter:只能增加的计数器
"""

from __future__ import annotations
from typing import Dict
from dataclasses import dataclass


class GCounter:
    """
    G-Counter (Grow-only Counter)
    
    每个节点维护自己的计数。
    合并时取各节点计数的最大值之和。
    
    特性:
    - 只能增加
    - 天然支持并发
    - 最终一致
    """
    
    def __init__(self, node_id: str):
        self.node_id = node_id
        self.counts: Dict[str, int] = {node_id: 0}
    
    def increment(self, amount: int = 1):
        """
        增加计数
        
        Args:
            amount: 增加量
        """
        self.counts[self.node_id] = self.counts.get(self.node_id, 0) + amount
    
    def get(self) -> int:
        """获取总值"""
        return sum(self.counts.values())
    
    def merge(self, other: GCounter):
        """
        合并另一个计数器
        
        对每个节点取最大值。
        """
        for node_id, count in other.counts.items():
            self.counts[node_id] = max(
                self.counts.get(node_id, 0),
                count
            )
    
    def to_dict(self) -> dict:
        return {
            'node_id': self.node_id,
            'counts': dict(self.counts)
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> GCounter:
        counter = cls(node_id=data['node_id'])
        counter.counts = data['counts']
        return counter
    
    def __repr__(self) -> str:
        return f"GCounter({self.get()})"

三、RGA:文本的核心 CRDT

3.1 RGA 原理

RGA 的核心思想:

每个字符都有一个唯一的 ID (node_id, seq)。
字符之间通过"前驱指针"链接形成链表。

插入操作:
  1. 为新字符生成唯一 ID
  2. 找到插入位置的前驱字符
  3. 将新字符插入到前驱之后

删除操作(墓碑):
  1. 标记字符为已删除(墓碑)
  2. 不真正删除,保留位置信息

合并规则:
  1. 如果两个字符 ID 相同,保留一个
  2. 如果两个字符插入到同一位置,按 ID 排序
  3. 删除标记可以安全合并

示例:
  初始: "He" (H:id1, e:id2)
  
  节点 A 插入 "l" 到位置 2:
    "Hel" (H:id1, e:id2, l:id3_A)
  
  节点 B 同时插入 "x" 到位置 2:
    "Hex" (H:id1, e:id2, x:id3_B)
  
  合并后(按 ID 排序):
    "Helx" 或 "Hexl"(取决于 ID 大小)

3.2 RGA 实现

# core/crdt/rga.py
"""
RGA (Replicated Growable Array)

基于链表的可增长数组,用于表示协同编辑的文本。
"""

from __future__ import annotations
from typing import List, Optional, Tuple, Dict
from dataclasses import dataclass, field
import uuid


@dataclass
class RGAPosition:
    """
    RGA 中的位置标识
    
    每个字符的唯一标识。
    """
    node_id: str       # 创建该字符的节点
    seq: int           # 节点内的序号
    
    def __lt__(self, other: RGAPosition) -> bool:
        """按 (seq, node_id) 排序"""
        if self.seq != other.seq:
            return self.seq < other.seq
        return self.node_id < other.node_id
    
    def __eq__(self, other: RGAPosition) -> bool:
        return self.node_id == other.node_id and self.seq == other.seq
    
    def __hash__(self) -> int:
        return hash((self.node_id, self.seq))
    
    def to_dict(self) -> dict:
        return {'node_id': self.node_id, 'seq': self.seq}
    
    @classmethod
    def from_dict(cls, data: dict) -> RGAPosition:
        return cls(node_id=data['node_id'], seq=data['seq'])


@dataclass
class RGANode:
    """
    RGA 链表节点
    
    代表一个字符或已删除的字符(墓碑)。
    """
    position: RGAPosition     # 唯一位置标识
    value: str                # 字符值
    deleted: bool = False     # 是否已删除(墓碑)
    next_pos: Optional[RGAPosition] = None  # 后继节点位置
    
    def to_dict(self) -> dict:
        return {
            'position': self.position.to_dict(),
            'value': self.value,
            'deleted': self.deleted,
            'next_pos': self.next_pos.to_dict() if self.next_pos else None
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> RGANode:
        return cls(
            position=RGAPosition.from_dict(data['position']),
            value=data['value'],
            deleted=data.get('deleted', False),
            next_pos=RGAPosition.from_dict(data['next_pos']) 
                     if data.get('next_pos') else None
        )


class RGA:
    """
    RGA (Replicated Growable Array)
    
    基于链表的 CRDT 文本类型。
    
    特性:
    - 插入操作天然可交换
    - 删除使用墓碑标记
    - 合并后自动收敛
    """
    
    def __init__(self, node_id: str):
        self.node_id = node_id
        
        # 节点映射: position -> RGANode
        self.nodes: Dict[RGAPosition, RGANode] = {}
        
        # 头节点(虚拟根节点)
        self.head = RGAPosition(node_id='__head__', seq=-1)
        
        # 序号计数器
        self.seq_counter = 0
        
        # 墓碑计数(用于统计)
        self.tombstone_count = 0
    
    # ---------- 本地操作 ----------
    
    def insert(self, index: int, value: str) -> List[RGANode]:
        """
        在指定位置插入字符串
        
        Args:
            index: 插入位置
            value: 要插入的字符串
        Returns:
            创建的节点列表
        """
        if index < 0:
            index = 0
        
        # 找到插入位置的前驱
        prev_pos = self._find_position(index)
        
        created_nodes = []
        for ch in value:
            # 生成新节点
            self.seq_counter += 1
            pos = RGAPosition(node_id=self.node_id, seq=self.seq_counter)
            
            node = RGANode(
                position=pos,
                value=ch,
                next_pos=None
            )
            
            # 插入到链表
            if prev_pos in self.nodes:
                node.next_pos = self.nodes[prev_pos].next_pos
                self.nodes[prev_pos].next_pos = pos
            
            self.nodes[pos] = node
            created_nodes.append(node)
            prev_pos = pos
        
        return created_nodes
    
    def delete(self, index: int, length: int = 1) -> List[RGAPosition]:
        """
        删除指定位置的字符
        
        Args:
            index: 起始位置
            length: 删除长度
        Returns:
            被删除节点的位置列表
        """
        deleted = []
        current = self.head
        
        # 遍历到起始位置
        pos_idx = 0
        while current:
            if current in self.nodes:
                node = self.nodes[current]
                if not node.deleted:
                    if pos_idx >= index and pos_idx < index + length:
                        node.deleted = True
                        self.tombstone_count += 1
                        deleted.append(current)
                    pos_idx += 1
            
            # 移动到下一个
            if current in self.nodes:
                current = self.nodes[current].next_pos
            else:
                break
        
        return deleted
    
    # ---------- 查询 ----------
    
    def get_text(self) -> str:
        """获取可见文本"""
        chars = []
        current = self.head
        
        while current:
            if current in self.nodes:
                node = self.nodes[current]
                if not node.deleted:
                    chars.append(node.value)
                current = node.next_pos
            else:
                break
        
        return ''.join(chars)
    
    def get_length(self) -> int:
        """获取可见文本长度"""
        return len(self.get_text())
    
    def _find_position(self, index: int) -> RGAPosition:
        """
        找到第 index 个可见字符的前驱位置
        
        Args:
            index: 目标索引
        Returns:
            前驱节点的位置
        """
        current = self.head
        visible_count = 0
        
        while current:
            if current in self.nodes:
                node = self.nodes[current]
                if not node.deleted:
                    if visible_count == index:
                        return current
                    visible_count += 1
                
                next_pos = node.next_pos
                if next_pos and next_pos in self.nodes:
                    current = next_pos
                else:
                    break
            else:
                break
        
        # 返回最后一个节点
        return current
    
    # ---------- 合并 ----------
    
    def merge(self, other: RGA):
        """
        合并另一个 RGA 副本
        
        合并规则:
        1. 相同位置的节点,保留一个
        2. 删除标记合并(任一标记删除即删除)
        3. 链表结构合并
        """
        # 合并所有节点
        for pos, node in other.nodes.items():
            if pos in self.nodes:
                # 已存在,合并删除标记
                if node.deleted:
                    self.nodes[pos].deleted = True
            else:
                # 新节点,直接添加
                self.nodes[pos] = RGANode(
                    position=pos,
                    value=node.value,
                    deleted=node.deleted,
                    next_pos=node.next_pos
                )
        
        # 更新墓碑计数
        self.tombstone_count = sum(
            1 for n in self.nodes.values() if n.deleted
        )
    
    # ---------- 序列化 ----------
    
    def to_dict(self) -> dict:
        return {
            'node_id': self.node_id,
            'seq_counter': self.seq_counter,
            'nodes': {
                pos.to_dict(): node.to_dict()
                for pos, node in self.nodes.items()
            },
            'head': self.head.to_dict()
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> RGA:
        rga = cls(node_id=data['node_id'])
        rga.seq_counter = data['seq_counter']
        
        for pos_data, node_data in data['nodes'].items():
            pos = RGAPosition.from_dict(
                eval(pos_data) if isinstance(pos_data, str) else pos_data
            )
            rga.nodes[pos] = RGANode.from_dict(node_data)
        
        return rga
    
    def garbage_collect(self, threshold: float = 0.3):
        """
        垃圾回收:清理墓碑
        
        当墓碑比例超过阈值时,重建链表。
        
        Args:
            threshold: 墓碑比例阈值
        """
        total = len(self.nodes)
        if total == 0:
            return
        
        tombstone_ratio = self.tombstone_count / total
        if tombstone_ratio < threshold:
            return
        
        # 重建:只保留可见节点
        text = self.get_text()
        self.nodes.clear()
        self.tombstone_count = 0
        self.seq_counter = 0
        
        # 重新插入
        self.insert(0, text)
    
    def __repr__(self) -> str:
        return f"RGA('{self.get_text()}', nodes={len(self.nodes)}, tombstones={self.tombstone_count})"

四、CRDT 文档引擎

4.1 CRDTDocument

# core/crdt/crdt_document.py
"""
基于 CRDT 的文档引擎
"""

from __future__ import annotations
from typing import List, Optional, Dict
from dataclasses import dataclass
import time
import logging

from core.crdt.rga import RGA, RGANode, RGAPosition
from core.crdt.lww_register import LWWRegister
from core.crdt.gcounter import GCounter

logger = logging.getLogger(__name__)


@dataclass
class DocumentSnapshot:
    """文档快照"""
    text: str
    version: int
    timestamp: float
    node_count: int
    
    def to_dict(self) -> dict:
        return {
            'text': self.text,
            'version': self.version,
            'timestamp': self.timestamp,
            'node_count': self.node_count
        }


class CRDTDocument:
    """
    基于 CRDT 的文档引擎
    
    使用 RGA 表示文本内容,LWWRegister 管理元数据。
    天然支持去中心化同步。
    """
    
    def __init__(self, document_id: str, node_id: str):
        self.document_id = document_id
        self.node_id = node_id
        
        # 文本内容(RGA)
        self.content = RGA(node_id)
        
        # 元数据(LWWRegister)
        self.title = LWWRegister(node_id, "Untitled")
        self.author = LWWRegister(node_id, "")
        
        # 操作计数
        self.op_count = GCounter(node_id)
        
        # 版本号
        self.version = 0
        
        # 变更回调
        self.on_change: Optional[callable] = None
    
    # ---------- 文本操作 ----------
    
    def insert(self, index: int, text: str) -> List[RGANode]:
        """
        插入文本
        
        Args:
            index: 位置
            text: 文本
        Returns:
            创建的节点
        """
        nodes = self.content.insert(index, text)
        self.op_count.increment()
        self.version += 1
        
        if self.on_change:
            self.on_change('insert', index, text)
        
        return nodes
    
    def delete(self, index: int, length: int = 1) -> List[RGAPosition]:
        """
        删除文本
        
        Args:
            index: 起始位置
            length: 删除长度
        Returns:
            被删除的位置
        """
        positions = self.content.delete(index, length)
        self.op_count.increment()
        self.version += 1
        
        if self.on_change:
            self.on_change('delete', index, length)
        
        return positions
    
    # ---------- 元数据操作 ----------
    
    def set_title(self, title: str):
        """设置标题"""
        self.title.set(title)
        self.version += 1
    
    def set_author(self, author: str):
        """设置作者"""
        self.author.set(author)
        self.version += 1
    
    # ---------- 查询 ----------
    
    def get_text(self) -> str:
        """获取文本"""
        return self.content.get_text()
    
    def get_length(self) -> int:
        """获取长度"""
        return self.content.get_length()
    
    def get_title(self) -> str:
        """获取标题"""
        return self.title.get() or "Untitled"
    
    def get_op_count(self) -> int:
        """获取操作数"""
        return self.op_count.get()
    
    # ---------- 合并 ----------
    
    def merge(self, other: CRDTDocument):
        """
        合并另一个文档副本
        
        Args:
            other: 另一个文档
        """
        # 合并文本内容
        self.content.merge(other.content)
        
        # 合并元数据
        self.title.merge(other.title)
        self.author.merge(other.author)
        
        # 合并操作计数
        self.op_count.merge(other.op_count)
        
        # 更新版本号
        self.version = max(self.version, other.version) + 1
        
        logger.info(f"Merged document: version={self.version}")
    
    # ---------- 快照 ----------
    
    def snapshot(self) -> DocumentSnapshot:
        """创建快照"""
        return DocumentSnapshot(
            text=self.get_text(),
            version=self.version,
            timestamp=time.time(),
            node_count=len(self.content.nodes)
        )
    
    # ---------- 序列化 ----------
    
    def to_dict(self) -> dict:
        return {
            'document_id': self.document_id,
            'node_id': self.node_id,
            'content': self.content.to_dict(),
            'title': self.title.to_dict(),
            'author': self.author.to_dict(),
            'op_count': self.op_count.to_dict(),
            'version': self.version
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> CRDTDocument:
        doc = cls(
            document_id=data['document_id'],
            node_id=data['node_id']
        )
        doc.content = RGA.from_dict(data['content'])
        doc.title = LWWRegister.from_dict(data['title'])
        doc.author = LWWRegister.from_dict(data['author'])
        doc.op_count = GCounter.from_dict(data['op_count'])
        doc.version = data.get('version', 0)
        return doc

五、CRDT 同步

5.1 同步协议

# core/crdt/sync.py
"""
CRDT 同步协议
"""

from __future__ import annotations
from typing import List, Optional, Dict
from dataclasses import dataclass
import time
import logging

from core.crdt.crdt_document import CRDTDocument
from core.crdt.rga import RGA

logger = logging.getLogger(__name__)


@dataclass
class SyncMessage:
    """同步消息"""
    node_id: str
    document_id: str
    changes: dict
    timestamp: float
    
    def to_dict(self) -> dict:
        return {
            'node_id': self.node_id,
            'document_id': self.document_id,
            'changes': self.changes,
            'timestamp': self.timestamp
        }


class CRDTSyncEngine:
    """
    CRDT 同步引擎
    
    管理 CRDT 文档的同步。
    由于 CRDT 的数学保证,只需要交换状态即可。
    """
    
    def __init__(self, document: CRDTDocument):
        self.document = document
        
        # 已知的其他节点状态
        self.known_nodes: Dict[str, int] = {}  # node_id -> last_version
    
    def generate_sync_message(self) -> SyncMessage:
        """
        生成同步消息
        
        包含完整的文档状态。
        """
        return SyncMessage(
            node_id=self.document.node_id,
            document_id=self.document.document_id,
            changes=self.document.to_dict(),
            timestamp=time.time()
        )
    
    def apply_sync_message(self, message: SyncMessage) -> bool:
        """
        应用同步消息
        
        Args:
            message: 同步消息
        Returns:
            是否有变更
        """
        if message.node_id == self.document.node_id:
            return False  # 忽略自己的消息
        
        # 重建文档
        remote_doc = CRDTDocument.from_dict(message.changes)
        
        # 合并
        before_version = self.document.version
        self.document.merge(remote_doc)
        
        changed = self.document.version > before_version
        
        if changed:
            self.known_nodes[message.node_id] = remote_doc.version
            logger.info(f"Applied sync from {message.node_id}: "
                       f"version {before_version} → {self.document.version}")
        
        return changed
    
    def get_sync_status(self) -> dict:
        """获取同步状态"""
        return {
            'node_id': self.document.node_id,
            'document_version': self.document.version,
            'known_nodes': len(self.known_nodes),
            'text_length': self.document.get_length()
        }

六、演示

6.1 CRDT 收敛演示

# examples/crdt_demo.py
"""
CRDT 收敛性演示
"""

import logging
import sys
import threading
import time

sys.path.insert(0, '..')

from core.crdt.crdt_document import CRDTDocument
from core.crdt.sync import CRDTSyncEngine

logging.basicConfig(level=logging.INFO)

def demonstrate_crdt_convergence():
    """演示 CRDT 的收敛性"""
    print("=" * 78)
    print("🧩 CRDT 收敛性演示")
    print("=" * 78)
    
    # 创建三个节点的文档
    doc_a = CRDTDocument("doc-1", "Alice")
    doc_b = CRDTDocument("doc-1", "Bob")
    doc_c = CRDTDocument("doc-1", "Carol")
    
    print(f"\n初始状态:")
    print(f"  Alice: '{doc_a.get_text()}'")
    print(f"  Bob:   '{doc_b.get_text()}'")
    print(f"  Carol: '{doc_c.get_text()}'")
    
    # 三个节点同时编辑
    print("\n--- 三人同时编辑 ---")
    
    # Alice 输入 "Hello"
    doc_a.insert(0, "Hello")
    print(f"  Alice 输入: 'Hello'")
    
    # Bob 输入 "World"
    doc_b.insert(0, "World")
    print(f"  Bob 输入: 'World'")
    
    # Carol 输入 "CRDT"
    doc_c.insert(0, "CRDT")
    print(f"  Carol 输入: 'CRDT'")
    
    print(f"\n同步前:")
    print(f"  Alice: '{doc_a.get_text()}'")
    print(f"  Bob:   '{doc_b.get_text()}'")
    print(f"  Carol: '{doc_c.get_text()}'")
    
    # 两两同步
    print("\n--- 开始同步 ---")
    
    sync_a = CRDTSyncEngine(doc_a)
    sync_b = CRDTSyncEngine(doc_b)
    sync_c = CRDTSyncEngine(doc_c)
    
    # Alice ↔ Bob
    msg_a = sync_a.generate_sync_message()
    sync_b.apply_sync_message(msg_a)
    msg_b = sync_b.generate_sync_message()
    sync_a.apply_sync_message(msg_b)
    print("  Alice ↔ Bob 同步完成")
    
    # Bob ↔ Carol
    msg_b = sync_b.generate_sync_message()
    sync_c.apply_sync_message(msg_b)
    msg_c = sync_c.generate_sync_message()
    sync_b.apply_sync_message(msg_c)
    print("  Bob ↔ Carol 同步完成")
    
    # Carol ↔ Alice
    msg_c = sync_c.generate_sync_message()
    sync_a.apply_sync_message(msg_c)
    msg_a = sync_a.generate_sync_message()
    sync_c.apply_sync_message(msg_a)
    print("  Carol ↔ Alice 同步完成")
    
    print(f"\n同步后(应完全一致):")
    print(f"  Alice: '{doc_a.get_text()}'")
    print(f"  Bob:   '{doc_b.get_text()}'")
    print(f"  Carol: '{doc_c.get_text()}'")
    
    consistent = (doc_a.get_text() == doc_b.get_text() == doc_c.get_text())
    print(f"\n一致性: {'✅ 一致!' if consistent else '❌ 不一致!'}")

def demonstrate_crdt_vs_ot():
    """演示 CRDT vs OT 的区别"""
    print("\n" + "=" * 78)
    print("⚔️ CRDT vs OT 对比演示")
    print("=" * 78)
    
    # CRDT 方式
    print("\n📌 CRDT 方式(去中心化):")
    alice = CRDTDocument("doc-2", "Alice")
    bob = CRDTDocument("doc-2", "Bob")
    
    alice.insert(0, "AB")
    bob.insert(0, "XY")
    
    # 直接合并
    alice.merge(bob)
    bob.merge(alice)
    
    print(f"  Alice: '{alice.get_text()}'")
    print(f"  Bob:   '{bob.get_text()}'")
    print(f"  一致: {alice.get_text() == bob.get_text()}")
    print(f"  无需中心服务器!")
    
    # 对比 OT(需要服务端)
    print("\n📌 OT 方式(需要中心服务端):")
    print("  需要服务端做操作变换")
    print("  需要维护操作顺序")
    print("  需要处理确认/重试")

def demonstrate_garbage_collection():
    """演示墓碑垃圾回收"""
    print("\n" + "=" * 78)
    print("🗑️  RGA 墓碑垃圾回收")
    print("=" * 78)
    
    doc = CRDTDocument("doc-3", "Demo")
    
    # 输入长文本
    doc.insert(0, "This is a long text that we will edit many times")
    print(f"\n初始: '{doc.get_text()}'")
    print(f"节点数: {len(doc.content.nodes)}")
    
    # 多次编辑产生墓碑
    print("\n--- 多次编辑 ---")
    for i in range(5):
        doc.delete(0, 5)
        doc.insert(0, "XXXXX")
        print(f"  编辑 {i+1}: '{doc.get_text()}'")
    
    print(f"\n编辑后:")
    print(f"  节点数: {len(doc.content.nodes)}")
    print(f"  墓碑数: {doc.content.tombstone_count}")
    print(f"  墓碑比例: {doc.content.tombstone_count/len(doc.content.nodes)*100:.1f}%")
    
    # 垃圾回收
    print("\n--- 垃圾回收 ---")
    doc.content.garbage_collect(threshold=0.2)
    print(f"  回收后节点数: {len(doc.content.nodes)}")
    print(f"  回收后墓碑数: {doc.content.tombstone_count}")
    print(f"  文本不变: '{doc.get_text()}'")

def main():
    demonstrate_crdt_convergence()
    demonstrate_crdt_vs_ot()
    demonstrate_garbage_collection()

if __name__ == "__main__":
    main()

七、测试

7.1 CRDT 测试

# tests/test_crdt.py
import pytest
from core.crdt.lww_register import LWWRegister, Timestamp
from core.crdt.gcounter import GCounter
from core.crdt.rga import RGA, RGAPosition, RGANode
from core.crdt.crdt_document import CRDTDocument
from core.crdt.sync import CRDTSyncEngine

class TestLWWRegister:
    """LWW-Register 测试"""
    
    def test_set_and_get(self):
        reg = LWWRegister("node-1")
        reg.set("hello")
        assert reg.get() == "hello"
    
    def test_merge(self):
        reg1 = LWWRegister("node-1", "hello")
        reg2 = LWWRegister("node-2", "world")
        
        # 确保 reg2 的时间戳更新
        import time
        time.sleep(0.001)
        reg2.set("world")
        
        reg1.merge(reg2)
        assert reg1.get() == "world"

class TestGCounter:
    """G-Counter 测试"""
    
    def test_increment(self):
        counter = GCounter("node-1")
        counter.increment()
        counter.increment(3)
        assert counter.get() == 4
    
    def test_merge(self):
        c1 = GCounter("node-1")
        c2 = GCounter("node-2")
        
        c1.increment(5)
        c2.increment(3)
        
        c1.merge(c2)
        assert c1.get() == 8

class TestRGA:
    """RGA 测试"""
    
    def test_insert_and_get(self):
        rga = RGA("node-1")
        rga.insert(0, "Hello")
        assert rga.get_text() == "Hello"
    
    def test_delete(self):
        rga = RGA("node-1")
        rga.insert(0, "Hello")
        rga.delete(0, 2)
        assert rga.get_text() == "llo"
    
    def test_merge_convergence(self):
        rga_a = RGA("Alice")
        rga_b = RGA("Bob")
        
        rga_a.insert(0, "AB")
        rga_b.insert(0, "XY")
        
        rga_a.merge(rga_b)
        rga_b.merge(rga_a)
        
        assert rga_a.get_text() == rga_b.get_text()

class TestCRDTDocument:
    """CRDT 文档测试"""
    
    def test_basic_operations(self):
        doc = CRDTDocument("doc-1", "Alice")
        
        doc.insert(0, "Hello")
        assert doc.get_text() == "Hello"
        
        doc.delete(0, 2)
        assert doc.get_text() == "llo"
    
    def test_merge_convergence(self):
        doc_a = CRDTDocument("doc-1", "Alice")
        doc_b = CRDTDocument("doc-1", "Bob")
        doc_c = CRDTDocument("doc-1", "Carol")
        
        # 三人同时编辑
        doc_a.insert(0, "Hello")
        doc_b.insert(0, "World")
        doc_c.insert(0, "CRDT")
        
        # 两两合并
        doc_a.merge(doc_b)
        doc_a.merge(doc_c)
        doc_b.merge(doc_a)
        doc_b.merge(doc_c)
        doc_c.merge(doc_a)
        doc_c.merge(doc_b)
        
        assert doc_a.get_text() == doc_b.get_text()
        assert doc_b.get_text() == doc_c.get_text()

if __name__ == "__main__":
    pytest.main([__file__, "-v"])

八、总结

8.1 本讲成果

组件

文件

功能

LWWRegister

core/crdt/lww_register.py

最后写入者胜出寄存器

GCounter

core/crdt/gcounter.py

只能增加的计数器

RGA

core/crdt/rga.py

可增长数组(文本核心)

CRDTDocument

core/crdt/crdt_document.py

基于 CRDT 的文档引擎

CRDTSyncEngine

core/crdt/sync.py

CRDT 同步引擎

8.2 核心知识点

  • CRDT 的本质:通过数据结构设计保证最终一致性,无需中心服务器

  • RGA 的工作原理:链表 + 唯一 ID + 墓碑标记

  • 合并的数学保证:无论操作顺序如何,最终结果一致

  • 墓碑机制:删除不真删,保留位置信息

  • 垃圾回收:定期清理墓碑,控制内存增长

8.3 下一讲预告

第7讲:选区同步与光标展示

我们将实现多人编辑时最重要的视觉反馈:

  • 远程光标渲染

  • 选区同步

  • 光标颜色管理

  • 平滑动画过渡

准备好了吗?让我们在第7讲再见!


🧰 开发之余的小工具推荐

处理 Base64、JWT 解析、JSON 格式化、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top(子页 PDF 大师:PDF 大师 - zz365工具箱)。所有计算在浏览器完成,文件不上传服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值