自定义文档并发控制
# 自定义文档并发控制
# 问题背景
在B/S架构下的办公系统中,用户访问请求都是并发的,也就是说经常会出现同时N个用户对一个服务器页面发出请求,这就有可能出现同一个文档被多个用户同时打开进行编辑的情况,那么,保存时文件就可能出现互相覆盖的问题。为什么会出现互相覆盖呢?举个简单例子,例如A用户先访问页面打开了一个文档开始编辑,这时B用户访问相同的页面打开了同一个文档也开始编辑,B用户可能很快就完成了文档修改工作并且保存到服务器。随后A用户也完成了工作并保存文档到服务器。这时,服务器上的这个文档已经变成了A用户修改的版本,B用户的修改被A用户的保存操作覆盖从而消失了。
# 两种解决方案
- 引入工作流模块。文档流转到哪个环节,就由哪个环节的用户去操作,其他用户无法打开此文档,或无法以编辑模式打开此文档。(互联网上有专门的工作流产品,因此本文不对此方案进行介绍。)
- 采用锁机制实现文档并发控制。在服务器端对文档进行加锁,只有被锁的用户才能打开此文档进行编辑,其他用户无法打开此文档,或无法以编辑模式打开此文档。
PageOffice V5及以前的版本自带了文档并发控制功能,设置PageOfficeCtrl对象的TimeSlice属性就可以保证同一时间同一篇文档只能由一个人打开,但是此功能仅限于单体Web项目且部署在一台服务器上。由于现在越来越多的项目使用了微服务架构或集群部署,因此就需要开发人员实现自定义的文档并发控制功能。下面我们以一个最简单的文档并发控制方案为例,介绍一下实现自定义的文档并发控制功能的主要步骤:
- 数据库设计调整,增加锁状态字段。在文档记录表中增加一个Editor字段,用于记录当前文档是否正被某用户编辑。如果该字段为空,表示文档未被任何用户编辑;如果非空,则字段值应为当前编辑者的用户名。
- 打开文件之前发送检查请求。当用户尝试打开文档时,前端应首先向后端发送一个请求,检查文档的Editor字段。
- 如果Editor字段为空,说明文档当前未被任何用户编辑,可以安全地允许当前用户打开文档,并将当前用户的用户名写入文档记录表中的Editor字段,以此标记文档当前处于被编辑状态。
- 如果Editor字段非空,即已经有其他用户正在编辑文档,则向用户显示提示信息,告知文档正在被他人编辑,建议稍后再试,或者以只读预览模式打开文档。
- 用户完成文档编辑并关闭文档时,前端应当通过Ajax请求通知后端,后端需将文档记录表中对应的Editor字段清空,从而释放文档的编辑锁。
# 后端代码
后端验证文档编辑状态的接口(比如:detectCurrentEditor.jsp),代码如下:
String id = request.getParameter("id");
String editor = "";
Class.forName("org.sqlite.JDBC");
String strUrl = "jdbc:sqlite:" + this.getServletContext().getRealPath("BingFa/BingFa.db");
Connection conn=DriverManager.getConnection(strUrl);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from doc where id="+id);
if(rs.next()){
editor = rs.getString("Editor");
}
rs.close();
stmt.close();
conn.close();
response.setContentType("application/json");
response.getWriter().print("{\"editor\":\""+ editor +"\"}"); //返回当前文档的编辑者
后端释放文档编辑锁的接口(比如:clearCurrentEditor.jsp),代码如下:
String id = request.getParameter("id");
Class.forName("org.sqlite.JDBC");
String strUrl = "jdbc:sqlite:" + this.getServletContext().getRealPath("BingFa/BingFa.db");
Connection conn=DriverManager.getConnection(strUrl);
Statement stmt2 = conn.createStatement();
stmt2.executeUpdate("Update doc set Editor='' where id="+id); //清空文档的编辑者
stmt2.close();
conn.close();
response.setContentType("application/json");
response.getWriter().print("{\"msg\":\"ok\"}");
# 前端代码
打开文件之前发送ajax请求到后端接口(比如:detectCurrentEditor.jsp)验证,检查当前文档是否有用户正在编辑。
// 编辑文件
function editFile(id){
var user = loginName; // 假设loginName为当前登录用户的用户名
// 检查当前文档是否有用户正在编辑
$.ajax({
url: 'detectCurrentEditor.jsp?id=' + id,
type: 'GET',
dataType: 'json',
success: function(data) {
if(data.editor == user || data.editor == ''){
POBrowser.openWindow('word.jsp?id='+id+'&user='+encodeURIComponent(user) , 'width=1200px;height=800px;');
}else{
alert('用户“'+data.editor+'”正在编辑此文档,请稍后重试,或点击“查看”只读打开。');
}
},
error: function(xhr, status, error) {
console.error('请求失败:', error);
}
});
}
关闭文件时,通过PageOffice的OnBeforeBrowserClosed()事件函数,发送ajax请求到后端接口(比如:clearCurrentEditor.jsp),将文档的Editor字段清空,释放编辑锁。
// 通知服务器端,用户已停止编辑文档
function SendCloseMsg(){
$.ajax({
url: 'clearCurrentEditor.jsp?id=<%=id%>',
type: 'GET',
dataType: 'json',
success: function(data) {
console.log('请求成功:', data);
},
error: function(xhr, status, error) {
console.error('请求失败:', error);
}
});
}
function OnBeforeBrowserClosed() {
// 此处可以执行窗口关闭前需要执行的业务逻辑代码
if(pageofficectrl.IsDirty){
if (confirm("提示:文档已被修改,是否继续关闭放弃保存 ?")) {
SendCloseMsg();
pageofficectrl.CloseWindow(true);//必须。否则窗口不会关闭。
}
}else{
SendCloseMsg();
pageofficectrl.CloseWindow(true);//必须。否则窗口不会关闭。
}
}
上次更新: 2025/11/07, 13:53:45