PHP项目在线编辑文档如何后端协同存储

wen PHP项目 30

本文目录导读:

PHP项目在线编辑文档如何后端协同存储

  1. 基于文件锁的简单方案(适合小型团队)
  2. 基于数据库的方案(适合中小规模)
  3. 基于OT(Operational Transformation)方案(适合实时协作)
  4. 基于CRDT(Conflict-free Replicated Data Types)方案
  5. 推荐架构:Redis + MySQL 混合方案
  6. 关键技术选型建议
  7. 前端配合示例(使用ShareJS)

为PHP项目实现在线文档编辑的后端协同存储,核心是解决并发冲突数据一致性问题,以下是几种成熟可行的方案,按复杂度从低到高排列:

基于文件锁的简单方案(适合小型团队)

// 使用 flock 进行文件锁定
class SimpleDocStorage {
    private $docPath = '/path/to/docs/';
    public function saveDoc($docId, $content, $userId) {
        $file = $this->docPath . $docId . '.json';
        $fp = fopen($file, 'c+');
        if (flock($fp, LOCK_EX)) { // 获取独占锁
            $data = [
                'content' => $content,
                'updated_by' => $userId,
                'updated_at' => time(),
                'version' => $this->getNextVersion($file)
            ];
            ftruncate($fp, 0);
            fwrite($fp, json_encode($data));
            fflush($fp);
            flock($fp, LOCK_UN);
        }
        fclose($fp);
        return true;
    }
}

基于数据库的方案(适合中小规模)

表结构设计

CREATE TABLE documents (
    id INT PRIMARY KEY AUTO_INCREMENT,VARCHAR(255),
    content LONGTEXT,
    version INT DEFAULT 1,
    created_by INT,
    updated_by INT,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    lock_token VARCHAR(64),  -- 用于实现乐观锁
    lock_expires_at TIMESTAMP
);
CREATE TABLE document_history (
    id INT PRIMARY KEY AUTO_INCREMENT,
    document_id INT,
    content TEXT,
    version INT,
    user_id INT,
    created_at TIMESTAMP,
    FOREIGN KEY (document_id) REFERENCES documents(id)
);

带版本控制的保存逻辑

class DatabaseDocStorage {
    private $db;
    public function saveWithVersionConflict($docId, $content, $userId, $expectedVersion) {
        $this->db->beginTransaction();
        try {
            // 乐观锁检查
            $stmt = $this->db->prepare(
                "UPDATE documents SET 
                    content = :content,
                    version = version + 1,
                    updated_by = :userId,
                    updated_at = NOW()
                 WHERE id = :docId AND version = :expectedVersion"
            );
            $stmt->execute([
                ':content' => $content,
                ':userId' => $userId,
                ':docId' => $docId,
                ':expectedVersion' => $expectedVersion
            ]);
            if ($stmt->rowCount() === 0) {
                throw new ConflictException('文档已被他人修改');
            }
            // 保存历史版本
            $this->saveHistory($docId, $content, $expectedVersion + 1, $userId);
            $this->db->commit();
            return ['version' => $expectedVersion + 1, 'success' => true];
        } catch (Exception $e) {
            $this->db->rollBack();
            throw $e;
        }
    }
}

基于OT(Operational Transformation)方案(适合实时协作)

使用 OT算法 实现类似Google Docs的实时协作:

// OT操作定义
class OTDocument {
    private $content = '';
    private $revision = 0;
    // 应用用户的操作
    public function applyOperation($operation, $clientVersion) {
        // 1. 转换客户端操作到服务器的当前版本
        $transformedOp = $this->transformOperation($operation, $clientVersion);
        // 2. 应用变换后的操作
        switch ($transformedOp->type) {
            case 'insert':
                $this->content = substr_replace(
                    $this->content, 
                    $transformedOp->text, 
                    $transformedOp->position, 
                    0
                );
                break;
            case 'delete':
                $this->content = substr_replace(
                    $this->content, 
                    '', 
                    $transformedOp->position, 
                    $transformedOp->length
                );
                break;
        }
        $this->revision++;
        return $this->revision;
    }
    // OT核心:操作转换
    private function transformOperation($operation, $clientVersion) {
        // 这里实现X(客户端操作)和Y(服务端累积操作)的转换
        // 实际项目建议使用成熟的OT库
        // ...
    }
}

基于CRDT(Conflict-free Replicated Data Types)方案

使用 YjsAutomerge 等成熟库:

// 后端存储状态(配合前端Yjs库)
class CRDTDocumentManager {
    private $redis;
    public function saveDocumentState($docId, $update) {
        // 保存CRDT状态(Yjs的压缩更新)
        $this->redis->append(
            "doc:{$docId}:updates",
            base64_encode($update)
        );
    }
    public function getDocumentState($docId) {
        // 获取所有更新记录
        $updates = $this->redis->get("doc:{$docId}:updates");
        if (!$updates) {
            return null;
        }
        // 解压并重建文档状态
        return base64_decode($updates);
    }
}

推荐架构:Redis + MySQL 混合方案

class HybridDocStorage {
    private $redis;
    private $db;
    private $lockPrefix = 'doc_lock:';
    // 获取编辑锁(防冲突)
    public function acquireEditLock($docId, $userId, $ttl = 30) {
        $lockKey = $this->lockPrefix . $docId;
        // 使用Redis SETNX确保原子性
        $locked = $this->redis->set(
            $lockKey,
            $userId,
            ['nx', 'ex' => $ttl]
        );
        if ($locked) {
            // 记录当前编辑者
            $this->redis->set("doc:{$docId}:editor", $userId);
            return true;
        }
        // 检查是否是当前用户
        $currentEditor = $this->redis->get($lockKey);
        return $currentEditor === $userId;
    }
    // 保存文档(带自动合并)
    public function saveDocument($docId, $content, $userId, $operation) {
        // 1. 获取文档当前状态
        $currentDoc = $this->redis->get("doc:{$docId}:current");
        // 2. 如果是实时协作,应用OT变换
        if ($currentDoc && $currentDoc['version'] !== $operation['clientVersion']) {
            $transformed = $this->transformOps($currentDoc, $operation);
            $newContent = $transformed['content'];
        } else {
            $newContent = $content;
        }
        // 3. 保存到Redis(快速响应)
        $this->redis->multi();
        $this->redis->set("doc:{$docId}:current", [
            'content' => $newContent,
            'version' => $currentDoc ? $currentDoc['version'] + 1 : 1
        ]);
        $this->redis->rPush("doc:{$docId}:history", json_encode([
            'content' => $newContent,
            'user_id' => $userId,
            'timestamp' => time()
        ]));
        $this->redis->exec();
        // 4. 异步写入MySQL(持久化)
        $this->queueAsyncSave($docId, $newContent, $userId);
        return [
            'version' => $currentDoc ? $currentDoc['version'] + 1 : 1,
            'success' => true
        ];
    }
}

关键技术选型建议

场景 推荐方案 原因
简单文档编辑 数据库+版本号 实现简单,足够用
多人协作编辑(<10人) OT算法 实时性高,冲突少
大规模协作(离线优先) CRDT(Yjs) 无冲突,天然支持
高并发编辑 Redis+异步持久化 读写快,不会丢数据

前端配合示例(使用ShareJS)

// 前端连接PHP后端WebSocket
const socket = new WebSocket('wss://your-server.com/doc-socket');
const doc = new sharejs.Doc('document-id');
doc.on('op', (ops, source) => {
  if (!source) {
    // 发送操作到服务器
    socket.send(JSON.stringify({
      type: 'operation',
      docId: doc.id,
      ops: ops,
      version: doc.version
    }));
  }
});
socket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'operation') {
    // 应用服务器传来的操作
    doc.receiveOp(msg.ops);
  }
};

推荐根据实际用户规模、实时性要求和团队技术栈选择合适方案,对于大多数PHP项目,Redis缓存 + MySQL持久化 + 版本控制是最实用的组合。

抱歉,评论功能暂时关闭!