问题
对于涉及文档编辑的应用,一个常见的应用场景就是多个用户试图修改同一个文档的问题。
某些情况下,多个用户可能先后读取同一个文档到本地浏览器,然后编辑修改。有的人可能只是需要简单修改,然后很快就保存离开;而有的用户可能需要修改多些内容,然后在前一个保存离开之后再存档离开。这种情景,如果不做任何处理,第二个离开的用户的存档会覆盖掉第一个用户的修改,我想这是第一个用户不愿意看到的。
方法
那么我想这里有两种解决办法:
- 合并两次修改。但是如果有冲突怎么办?像 Git 那样修改 conflicts?这个办法还可能需要多个用户沟通解决,实现起来也比较复杂。
- 锁定。只让一人修改,后来者只能读,并被告知某人正在编辑,稍后再来。这个办法简单,容易实现。
实现
我决定使用 socket.io 来实现第二个方案。websocket 的好处是实时,不用轮询来得知状态。代码也简洁。虽然这里使用 Ajax 方式也可以。
具体方法是(只考虑较少用户量):
-
客户端(浏览器):每次试图从服务器读取一个文档时,发送
socket.emit("toEdit", {userEmail: $scope.userEmail, docId: $scope.docId});
等下会介绍,服务器端会有一个对象保存所有正在使用的相关文档信息,这个 docId 用来判断这份文档是否正在使用,userEmail 判断谁在占用该文档,可以方便内部用户之间协商使用时间,例如企业内部用户。
如果发现这份文档正在被其他人使用,服务器端会发送 ‘isEditing’ 消息告知客户端正在使用
socket.on('isEditing', function (email) { alert("该文档正在被 " + email + " 编辑。”); $scope.disableSaveButton = true; //灰化保存按钮,防止用户保存更新 });
-
服务器端(Node.js):在 server 端使用一个对象保存已在编辑的相关信息。如下:
//结构:{socket.id: {userEmail: email, docId: id}} //socket.id: 每个 socket 链接自动产生的 id,用于识别每个登录的页面,同个用户可能使用多个页面登录; //doc id: 文档的唯一ID, 用于识别用户准备和正在编辑的文档; //user email: 用于通知用户谁正占用该文档。如果是内部用户,这可以让他们自己协调使用时间。 var editingUsersInfo = {};
editingUsersInfo 保存了所有正在编辑文档的相关信息;如果一个页面查找的 docId 没人在使用的话,这个对象就会产生一个对应的属性。见下:
io.on('connection', function(socket){
socket.on('toEdit', function(c_ei){
//s_ei: server {user email, doc id}; 服务器端每个 socket 链接对应的正在使用的信息
//c_ei: client {user email, doc id}; 客户端试图编辑某个文档的信息
//found_c_ei: 发现正处于编辑状态的客户和文档信息;
//ssid: server socket id
var found_c_ei = _.find( editingUsersInfo, function( s_ei, ssid ){
//只有文档 id 一样并且同时 socket id 不一样时才算该文档正在被编辑;
//如果文档 id 和 socket id 都各自一样,那么说明是正在使用的用户刷新了或者在重新读取了该文档
if( s_ei.babyID === c_ei.babyID && (ssid != socket.id)){
socket.emit( "isEditing", s_ei.email );
if( editingUsersInfo.hasOwnProperty(socket.id) ){
delete editingUsersInfo[socket.id]; }//这里是去掉已有用户搜索其他文档时遇到已在使用的情况
return true;
}
});
if( !found_c_ei ){
editingUsersInfo[socket.id] = c_ei; //不在使用,保存起来
}
});
socket.on("disconnect", function(){
delete editingUsersInfo[socket.id]; //离线切断链接,删除保存的信息
});
});
辅助
为了防止有人打开文档之后忘记关闭或者退出,一直占用文档的情况发生,可以通过监测用户是否使用来设计倒计时,例如半小时没有任何鼠标键盘动作,那么就自动把用户登出。我使用的是这个 angular 包:http://hackedbychinese.github.io/ng-idle/ 。
总结
很简单的实现。因为 Node.js 的单线程特性,我们也不用考虑多个用户同时访问数据库的情况。写过多线程程序的人应该知道这种 race condition 问题有多麻烦。
单线程 跟 多个用户同时访问数据库 有什么关系?
@hezedu 在多线程的系统,每个用户可能是一个线程,如果都去check editingUsersInfo,虽然都是一个 docId,但是有可能都没有发现对方在使用,然后把自己保存在 editingUsersInfo 里。单线程的话,肯定有一先一后的顺序。
@russj node仿问数据库不都是异步吗?
设置资源锁是对的,我比较倾向于如果资源占用editing(user1),其他的users对于资源都是不可编辑的,直到user1释放资源。
我也是加了个锁定的旗子
只有一个进程,但是一旦要做大呢? 自豪地采用 CNodeJS ionic
@hezedu 异步的话返回也是有先后的,只要是一条线就没有问题。关键只要不是在做了判断后值被其他线程改了,不然就被坑。
@fengmk2 还没有做大。你有没有经验怎么做?比如有千万用户之类的
不错的悲观锁,的确多进程之后协调貌似还挺头疼的?
可以考虑用memcached或redis实现多进程间的资源竞争。