本文目录导读:

- 基于文件锁的简单方案(适合小型团队)
- 基于数据库的方案(适合中小规模)
- 基于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持久化 + 版本控制是最实用的组合。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播