本文目录导读:

- 基于文件锁的简单方案(适合小型团队)
- 基于数据库的方案(适合中小规模)
- 基于OT(Operational Transformation)方案(适合实时协作)
- 基于CRDT(Conflict-free Replicated Data Types)方案
- 推荐架构:Redis + MySQL 混合方案
- 关键技术选型建议
- 前端配合示例(使用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)方案
使用 Yjs 或 Automerge 等成熟库:
// 后端存储状态(配合前端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持久化 + 版本控制是最实用的组合。