一、数据同步冲突的成因与危害

在 SiYuan 笔记系统的多设备协同场景中,数据同步冲突是常见问题。当多个客户端同时修改同一资源、网络延迟导致同步顺序错乱或设备时钟不一致时,极易引发数据覆盖、内容丢失等问题。这些冲突不仅影响用户体验,更可能造成重要笔记数据的损坏。

冲突产生的核心原因包括:

  • 并发编辑:多设备同时修改同一文档
  • 网络不稳定:同步请求延迟导致版本混乱
  • 设备差异:本地缓存与服务器数据不同步
  • 同步策略缺陷:缺乏有效的版本校验机制

二、冲突预防的核心原则

针对 SiYuan 数据同步场景,我们需要遵循以下原则设计冲突预防机制:

  1. 版本追踪:为每个资源维护唯一版本标识
  2. 增量同步:仅传输修改部分而非完整数据
  3. 原子操作:确保同步过程的完整性
  4. 冲突检测:提前发现潜在的同步冲突
  5. 明确策略:定义清晰的冲突解决规则

三、PHP 实现的核心技术方案

1. 数据结构设计

首先设计基础数据结构,包含必要的版本控制字段:

php

<?php
/**
 * SiYuan笔记数据模型
 * 包含版本控制所需的核心字段
 */
class NoteData {
    // 笔记唯一标识
    public $noteId;
    // 笔记内容
    public $content;
    // 版本号(用于乐观锁)
    public $version;
    // 最后修改时间戳
    public $lastModified;
    // 最后修改设备ID
    public $lastModifiedBy;
    // 数据校验哈希
    public $contentHash;
    
    /**
     * 生成内容哈希用于快速比对
     * @return string
     */
    public function generateHash() {
        return sha1($this->content . $this->lastModified);
    }
}
?>

2. 版本控制实现

采用乐观锁机制实现版本控制,确保数据更新的原子性:

php

<?php
/**
 * 笔记同步管理器
 * 实现版本控制与冲突检测
 */
class NoteSyncManager {
    private $db; // 数据库连接
    
    /**
     * 初始化数据库连接
     */
    public function __construct($dbConnection) {
        $this->db = $dbConnection;
    }
    
    /**
     * 尝试更新笔记,使用乐观锁防止冲突
     * @param NoteData $note 待更新的笔记数据
     * @return array 包含操作结果和冲突信息的数组
     */
    public function updateNote(NoteData $note) {
        try {
            // 开启数据库事务
            $this->db->beginTransaction();
            
            // 查询当前服务器上的笔记版本
            $stmt = $this->db->prepare("
                SELECT version, last_modified FROM notes 
                WHERE note_id = :noteId FOR UPDATE
            ");
            $stmt->execute([':noteId' => $note->noteId]);
            $serverNote = $stmt->fetch(PDO::FETCH_ASSOC);
            
            // 检查是否存在版本冲突
            if ($serverNote && $serverNote['version'] != $note->version) {
                // 发现冲突,回滚事务
                $this->db->rollBack();
                return [
                    'success' => false,
                    'conflict' => true,
                    'serverVersion' => $serverNote['version'],
                    'message' => '检测到版本冲突,请先同步最新数据'
                ];
            }
            
            // 计算新的版本号和哈希值
            $newVersion = $serverNote ? $serverNote['version'] + 1 : 1;
            $note->version = $newVersion;
            $note->lastModified = time();
            $note->contentHash = $note->generateHash();
            
            // 执行更新或插入操作
            if ($serverNote) {
                // 更新现有笔记
                $updateStmt = $this->db->prepare("
                    UPDATE notes SET 
                        content = :content,
                        version = :version,
                        last_modified = :lastModified,
                        last_modified_by = :lastModifiedBy,
                        content_hash = :contentHash
                    WHERE note_id = :noteId AND version = :oldVersion
                ");
                $updateStmt->execute([
                    ':content' => $note->content,
                    ':version' => $newVersion,
                    ':lastModified' => $note->lastModified,
                    ':lastModifiedBy' => $note->lastModifiedBy,
                    ':contentHash' => $note->contentHash,
                    ':noteId' => $note->noteId,
                    ':oldVersion' => $serverNote['version']
                ]);
            } else {
                // 插入新笔记
                $insertStmt = $this->db->prepare("
                    INSERT INTO notes 
                    (note_id, content, version, last_modified, last_modified_by, content_hash)
                    VALUES (:noteId, :content, :version, :lastModified, :lastModifiedBy, :contentHash)
                ");
                $insertStmt->execute([
                    ':noteId' => $note->noteId,
                    ':content' => $note->content,
                    ':version' => $newVersion,
                    ':lastModified' => $note->lastModified,
                    ':lastModifiedBy' => $note->lastModifiedBy,
                    ':contentHash' => $note->contentHash
                ]);
            }
            
            // 提交事务
            $this->db->commit();
            return [
                'success' => true,
                'conflict' => false,
                'newVersion' => $newVersion,
                'message' => '笔记更新成功'
            ];
            
        } catch (PDOException $e) {
            // 发生错误时回滚
            $this->db->rollBack();
            return [
                'success' => false,
                'conflict' => false,
                'message' => '数据库错误:' . $e->getMessage()
            ];
        }
    }
}
?>

3. 增量同步实现

为减少数据传输量,实现增量同步机制:

php

<?php
/**
 * 增量同步处理器
 * 仅传输和处理修改的数据部分
 */
class IncrementalSyncProcessor {
    private $syncManager;
    
    public function __construct(NoteSyncManager $manager) {
        $this->syncManager = $manager;
    }
    
    /**
     * 获取指定时间点后的所有变更
     * @param int $sinceTimestamp 起始时间戳
     * @return array 变更的笔记列表
     */
    public function getChangesSince($sinceTimestamp) {
        // 实际项目中这里会查询数据库获取变更记录
        // 仅返回修改过的笔记ID和版本信息,而非完整内容
        $changes = [
            // 示例数据结构
            /*
            [
                'noteId' => 'note123',
                'version' => 3,
                'lastModified' => 1620000000,
                'isDeleted' => false
            ],
            */
        ];
        return $changes;
    }
    
    /**
     * 处理客户端提交的增量变更
     * @param array $localChanges 本地变更列表
     * @param int $clientLastSyncTime 客户端最后同步时间
     * @return array 同步结果
     */
    public function processLocalChanges($localChanges, $clientLastSyncTime) {
        $results = [];
        $serverChanges = $this->getChangesSince($clientLastSyncTime);
        
        // 检测冲突:比较客户端和服务器的变更
        $conflicts = $this->detectConflicts($localChanges, $serverChanges);
        
        if (!empty($conflicts)) {
            // 存在冲突,返回冲突信息给客户端
            return [
                'success' => false,
                'conflicts' => $conflicts,
                'serverChanges' => $serverChanges,
                'message' => '检测到同步冲突,请解决后重试'
            ];
        }
        
        // 无冲突,处理所有本地变更
        foreach ($localChanges as $change) {
            $note = new NoteData();
            // 填充笔记数据...
            $result = $this->syncManager->updateNote($note);
            $results[] = $result;
        }
        
        return [
            'success' => true,
            'results' => $results,
            'serverChanges' => $serverChanges,
            'newSyncTime' => time()
        ];
    }
    
    /**
     * 检测客户端与服务器之间的变更冲突
     * @param array $localChanges 本地变更
     * @param array $serverChanges 服务器变更
     * @return array 冲突列表
     */
    private function detectConflicts($localChanges, $serverChanges) {
        $conflicts = [];
        $serverNoteIds = array_column($serverChanges, 'noteId');
        
        // 检查本地修改的笔记是否在服务器上也被修改
        foreach ($localChanges as $localChange) {
            if (in_array($localChange['noteId'], $serverNoteIds)) {
                $conflicts[] = $localChange['noteId'];
            }
        }
        
        return $conflicts;
    }
}
?>

4. 冲突解决策略

当检测到冲突时,需要有明确的解决策略:

php

<?php
/**
 * 冲突解决处理器
 * 提供多种冲突解决策略
 */
class ConflictResolver {
    /**
     * 解决笔记冲突的方法
     * @param NoteData $localNote 本地笔记
     * @param NoteData $serverNote 服务器笔记
     * @param string $strategy 解决策略
     * @return NoteData 解决后的笔记
     */
    public function resolveConflict(NoteData $localNote, NoteData $serverNote, $strategy = 'server_wins') {
        switch ($strategy) {
            case 'server_wins':
                // 服务器版本优先
                return $serverNote;
                
            case 'local_wins':
                // 本地版本优先
                return $localNote;
                
            case 'merge':
                // 尝试自动合并(适用于支持分段的文档)
                return $this->autoMergeNotes($localNote, $serverNote);
                
            case 'create_copy':
                // 创建副本保留两个版本
                $copyNote = clone $serverNote;
                $copyNote->noteId = $serverNote->noteId . '_conflict_copy_' . time();
                $copyNote->content = "【冲突副本】\n" . $serverNote->content;
                return $copyNote;
                
            default:
                return $serverNote;
        }
    }
    
    /**
     * 自动合并两个笔记内容
     * 简单实现:保留双方修改并标记冲突区域
     */
    private function autoMergeNotes(NoteData $localNote, NoteData $serverNote) {
        $merged = new NoteData();
        $merged->noteId = $localNote->noteId;
        $merged->version = max($localNote->version, $serverNote->version) + 1;
        $merged->lastModified = time();
        
        // 实际应用中需要更复杂的合并逻辑
        // 这里仅做简单示例
        $merged->content = "【自动合并结果】\n\n" .
                          "本地修改:\n{$localNote->content}\n\n" .
                          "服务器修改:\n{$serverNote->content}\n\n" .
                          "【请检查并编辑合并内容】";
                          
        return $merged;
    }
}
?>

四、最佳实践总结

  1. 分层防御策略

    • 前端:实时检测本地修改,定期自动保存
    • 传输层:使用增量同步减少冲突概率
    • 服务层:实现严格的版本控制和原子操作
    • 存储层:保留历史版本,支持数据回溯
  2. 用户体验优化

    • 提前预警:当检测到其他设备正在编辑时提示用户
    • 自动同步:设置合理的自动同步间隔
    • 清晰提示:冲突发生时提供明确的解决选项
    • 简化操作:提供一键合并和版本对比功能
  3. 系统设计建议

    • 实现定期全量同步作为增量同步的补充
    • 对重要操作保留详细日志,便于问题排查
    • 设计合理的缓存策略,减少重复同步
    • 考虑分布式锁机制处理高并发场景

通过上述机制,我们可以在 SiYuan 笔记系统中有效预防和处理数据同步冲突,保障用户数据的完整性和一致性。实际应用中,还需要根据具体业务场景调整策略,平衡性能与数据安全性。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐