如何设计一个多用户的Ajax web应用程序以实现并发安全

96

我有一个网页,显示了来自服务器的大量数据,通信是通过ajax完成的。

每当用户与此数据交互并更改它时(例如用户A重命名某些内容),它会告诉服务器执行该操作,然后服务器将返回新更改的数据。

如果用户B同时访问该页面并创建一个新的数据对象,它将再次通过ajax告诉服务器,服务器将返回新对象给用户。

在A的页面上,我们有一个重命名对象的数据。在B的页面上,我们有一个具有新对象的数据。在服务器上,数据既有重命名对象又有新对象。

在多个用户同时使用页面时,如何保持页面与服务器同步?

锁定整个页面或在每次更改时将整个状态转储到用户端等选项应该避免。

如果有帮助的话,在这个特定的例子中,web页面调用一个静态web方法,在数据库上运行一个存储过程。存储过程将返回任何它已更改的数据但不再返回其他数据。静态web方法随后将存储过程的返回转发给客户端。

悬赏编辑:

如何设计一个多用户的Web应用程序,它使用Ajax与服务器进行通信,但避免并发问题?

即:并发访问功能和数据库数据,而没有任何数据或状态损坏的风险。


不太确定,但是您可以创建一个类似Facebook的页面,其中浏览器会不断发送Ajax请求以寻找服务器数据库中的更改并在浏览器上更新它们。 - S L
将客户端状态序列化,然后通过ajax告诉服务器“这是我的状态,我需要更新什么”是一种选择。但需要客户端知道如何在一个地方更新所有信息的每一位。 - Raynos
1
用户端并发的最佳解决方案是否仅仅是推送变体之一?Websockets、comet等。 - davin
@davin 可能确实如此。但我不熟悉彗星,而且WebSockets并不支持跨浏览器。 - Raynos
2
有很好的跨浏览器支持包,特别是我推荐socket.io,虽然还有jWebSocket和许多其他包可供选择。如果您选择socket.io,您可以整合各种node.js好用的工具,如框架和(客户端)模板引擎等。 - davin
如果您需要对相同数据进行实时并发编辑,请查看ShareJS - nornagon
8个回答

159

概览:

  • 介绍
  • 服务器架构
  • 客户端架构
  • 更新案例
  • 提交案例
  • 冲突案例
  • 性能和可扩展性

嗨,Raynos,

我不会在这里讨论任何特定的产品。其他人提到的是已经看过的好工具集(也许应该将node.js添加到该列表中)。

从架构的角度来看,您似乎遇到了版本控制软件中可见的同样问题。一个用户对对象进行更改,另一个用户希望以另一种方式更改相同的对象 => 冲突。您必须将用户对对象的更改整合起来,同时能够及时且高效地交付更新,检测并解决上述冲突等问题。

如果我站在你的立场上,我会开发像这样的东西:

1. 服务器端:

  • 确定一个合理的级别,您可以将其定义为“原子构件”(页面?页面上的对象?对象内的值?)。这将取决于您的Web服务器、数据库和缓存硬件、用户数量、对象数量等,这样做并不容易。

  • 对于每个原子构件,具有:

    • 应用程序范围内的唯一ID
    • 递增版本ID
    • 用于写访问的锁定机制(可能是互斥体)
    • 一个小的历史记录或“更改日志”,在环形缓冲区中(共享内存对这些工作效果很好)。单个键值对可能也可以,但扩展性较差。参见http://en.wikipedia.org/wiki/Circular_buffer
  • 能够有效地向连接的用户交付相关更改日志的服务器或伪服务器组件。观察者模式是您的朋友。

2. 客户端端:

  • 能够与上述服务器保持长时间运行的HTTP连接的JavaScript客户端,或使用轻量级轮询。

  • 当连接的JavaScript客户端通知已更改监视的构件历史记录时,刷新网站内容的JavaScript构件更新程序组件。(再次,观察者模式可能是一个不错的选择)

  • JavaScript构件提交程序组件,它可能请求更改原子构件,尝试获取互斥锁。它将通过比较已知的客户端构件版本ID和当前服务器端构件版本ID来检测是否在几秒钟前(JavaScript客户端的延迟和提交过程的因素)更改了构件的状态。

  • JavaScript冲突解决程序,允许人类做出哪个更改是正确的决定。您可能不想只告诉用户“有人比你更快。我删除了你的更改。去哭吧。”从相当技术的差异或更用户友好的解决方案中选择。

那么该如何进行呢...

案例1:更新的序列图

  • 浏览器渲染页面
  • JavaScript“看到”具有至少一个值字段、唯一标识符和版本ID的部件
  • JavaScript客户端启动,请求“关注”这些找到的构件历史记录,从它们发现的版本开始(旧的更改不重要)
  • 服务器进程注意到请求并持续检查和/或发送历史记录
  • 历史条目可能包含简单的通知“构件x已更改,请客户端请求数据”,允许客户端独立轮询或完整数据集“构件x已更改为值foo”
  • JavaScript构件更新程序尽其所能地获取已知更新的新值。它执行新的ajax请求或被JavaScript客户端提供。
  • 页面DOM内容更新,用户选择性地得到通知。历史记录继续被监视。

案例2:提交

  • 构件提交者知道用户输入的期望新值,并向服务器发送更改请求
  • 服务器端互斥锁被获取
  • 服务器接收“嘿,我知道构件x的状态是从版本123开始的,让我把它设置为值foo。”
  • 如果服务器端版本的构件x等于(不能小于)123,则新值将被接受,生成新版本ID 124。
  • 新状态信息“更新为版本124”和可选的新值foo放置在构件x的环形缓冲区(更改日志/历史记录)的开头
  • 服务器端互斥锁被释放
  • 请求构件提交者很高兴收到提交确认和新ID。
  • 同时,服务器端服务器组件继续轮询/推送环形缓冲区到连接的客户端。所有观看构件x的缓冲区的客户端都将在其通常的延迟内获得新的状态信息和值(请参见案例1。)

案例3:冲突处理

  • 构件提交者知道用户输入的期望新值,并向服务器发送更改请求
  • 同时,另一个用户已成功更新了相同的构件(请参见案例2。)但由于各种延迟,这对我们其他用户来说仍然是未知的。
  • 因此,服务器端互斥锁被获取(或等待,直到“更快”的用户提交他的更改)
  • 服务器接收“嘿,我知道构件x的状态是从版本123开始的,让我把它设置为值foo。”
  • 在服务器端,构件x的版本现在已经是124了。请求的客户端无法知道他将要覆盖的值。
  • 显然,服务器必须拒绝更改请求(不计入上帝干预的覆盖优先级),释放互斥锁,并足够友好地直接向客户端发送新的版本ID和新值。
  • 面对被拒绝的提交请求和更改请求用户尚未知道的值,JavaScript构件提交者引用冲突解决器,该解决器显示并解释问题给用户。
  • 通过智能冲突解决器JS提供一些选项,允许用户再次尝试更改值。
  • 一旦用户选择了他认为正确的值,该过程将从案例2开始(如果有人更快则从案例3开始)克里斯托弗·斯特拉森


1
@ChristophStrasen 看看像node.js这样的事件驱动服务器,它不依赖于每个用户一个线程。这使得推送技术可以以更低的内存消耗来处理。我认为使用node.js服务器并依赖TCP WebSockets真的有助于扩展。但这完全破坏了跨浏览器兼容性。 - Raynos
2
+500,你绝对赢了这个。这是一个很棒的答案。 - Raynos
1
@luqmaan,这个答案是来自2011年2月的。Websockets当时还是新奇事物,在Chrome中直到8月份才正式发布。不过,Comet和socket.io都还不错,我认为那只是一个更有效的方法的建议。 - Ricardo Tomasi
1
如果Node.js对你来说有点超出了舒适区(或者运维团队的舒适区,但是确定问题的业务背景),你也可以使用基于Java的服务器的Socket.io。Tomcat和Jetty都支持无线程连接以进行服务器推送设置(例如:http://wiki.eclipse.org/Jetty/Feature/Continuations)。 - Tomas
作为额外的评论,我想提到一种技术,在这个讨论中还没有被提到。那就是支持ETAG作为并发控制的形式怎么样?http://fideloper.com/laravel4-etag-conditional-get有一篇文章从服务器端的角度解释了这个问题,与RESTful API相关。 - Abba Bryant
显示剩余6条评论

13

我知道这是一个老问题,但我想加入一些意见。

OT(操作转换)似乎非常适合您需要进行并发和一致的多用户编辑的要求。它是谷歌文档(Google Docs)中使用的技术,也曾用于Google Wave:

有一个基于JS的库,用于使用操作转换-ShareJS (http://sharejs.org/),由来自Google Wave团队的成员编写。

如果您愿意,还有一个完整的MVC Web框架-DerbyJS(http://derbyjs.com/),基于ShareJS构建,可为您完成所有任务。

它使用BrowserChannel在服务器和客户端之间进行通信(我相信WebSocket支持正在进行中-先前通过Socket.IO完成了此功能,但由于开发人员与Socket.io的问题而被取出)。初学者文档目前有点少。


5
我建议为每个数据集添加基于时间的修改时间戳。因此,如果您正在更新数据库表,则应相应更改修改时间戳。使用AJAX,可以将客户端的修改时间戳与数据源的时间戳进行比较-如果用户落后时间,更新显示。类似于此站点在您输入答案时定期检查问题是否有人回答的方式。

这是一个有用的观点。它也帮助我更从设计角度理解我们数据库中的“LastEdited”字段。 - Raynos
没错。该网站使用“心跳”,意思是每隔一段时间它会向服务器发送一个AJAX请求,并传递要检查的数据ID以及它所拥有的修改时间戳。假设我们现在在第1029个问题上。每次AJAX请求,服务器只查看第1029个问题的修改时间戳。如果它发现客户端拥有旧版本的数据,它会通过心跳回复一个新的副本。客户端可以重新加载页面(刷新),或者显示某种消息来警告用户有新的数据。 - Chris Baker
修改后的标记比使用哈希算法对当前“数据”进行哈希并将其与另一侧的哈希值进行比较要好得多。 - Raynos
1
请记住,客户端和服务器必须有访问完全相同的时间,以避免不一致性。 - prayerslayer

3
您需要使用推送技术(也称为Comet或反向Ajax)来在将更改保存到数据库后立即将更改传播给用户。目前最好的技术似乎是Ajax长轮询,但并非每个浏览器都支持,因此需要备选方案。幸运的是,已经有解决方案可以为您处理这些问题。其中包括:orbited.org和先前提到的socket.io。
未来将会有一种更简单的方法称为WebSockets,但目前尚不确定该标准何时将准备就绪以供使用,因为当前的标准存在安全问题。
数据库中不应该出现新对象的并发问题。但是,当用户编辑对象时,服务器需要一些逻辑来检查对象是否已被同时编辑或删除。如果对象已被删除,则解决方案很简单:只需放弃编辑。
但最困难的问题出现了,当多个用户同时编辑同一个对象时。如果用户1和2同时开始编辑一个对象,他们将在同一数据上进行编辑。假设用户1的更改先发送到服务器,而用户2仍在编辑数据。然后您有两个选择:您可以尝试将用户1的更改合并到用户2的数据中,或者您可以告诉用户2他的数据已过期,并在他的数据被发送到服务器时向他显示错误消息。后者在这里不太用户友好,但前者很难实现。
少数几个真正做对这件事情的实现之一是EtherPad,后来被Google收购。我相信他们随后在Google Docs和Google Wave中使用了EtherPad的某些技术,但我不能确定。Google还开源了EtherPad,所以根据您要做什么,也许值得一看。

同时编辑是一件非常困难的事情,因为由于延迟问题,无法在Web上进行原子操作。也许this article可以帮助您更深入地了解这个话题。


2
尝试自己编写这些内容是一项艰巨的工作,而且很难做到完美。其中一个选择是使用一个专门用于实时保持客户端与数据库以及彼此同步的框架。
我发现 Meteor 框架在这方面表现良好(http://docs.meteor.com/#reactivity)。
"Meteor 倡导响应式编程的概念。这意味着您可以以简单的命令式风格编写代码,当数据发生变化时,结果将自动重新计算,而这些数据是您的代码所依赖的。"
"这种简单的模式(响应式计算 + 响应式数据源)具有广泛的适用性。程序员无需编写取消订阅/重新订阅调用,并确保它们在正确的时间被调用,消除了整个数据传播代码类别,否则会用错误倾向的逻辑拥堵您的应用程序。"

1
我不敢相信没有人提到Meteor。这是一个新的、不成熟的框架(只正式支持一种数据库),但它可以帮助你摆脱多用户应用程序中的所有麻烦和思考。事实上,你不能不构建一个多用户实时更新的应用程序。以下是一个简要概述:
  • 所有内容都在node.js(JavaScript或CoffeeScript)中,因此您可以在客户端和服务器之间共享像验证这样的东西。
  • 它使用WebSockets,但可以回退到旧版浏览器。
  • 它专注于对本地对象的即时更新(例如,UI感觉灵敏),并将更改发送到后台服务器。只允许原子更新,以使混合更新更简单。在服务器上被拒绝的更新将被回滚。
  • 作为奖励,它为您处理实时代码重新加载,并在应用程序发生根本性变化时保留用户状态。
Meteor非常简单,我建议您至少查看它以获取可借鉴的想法。

1
我非常喜欢Derby和Meteor的想法,适用于某些类型的应用程序。但是,文件/记录所有权和权限是一些现实世界中尚未解决的问题。此外,从长期使用微软的经验来看,让80%变得非常容易,并花费太多时间处理另外20%的问题,我不愿意使用这样的PFM(纯粹的魔法)解决方案。 - Tracker1

1
这些维基百科页面可能有助于了解并发并行计算的相关知识,以便设计一个Ajax Web应用程序,该应用程序可以拉取推送状态事件(EDA)消息消息模式中。基本上,消息被复制到通道订阅者中,它们响应更改事件和同步请求。

有许多形式的并发基于Web的协作软件。

有许多HTTP API客户端库可用于etherpad-lite,这是一款协作实时编辑器。

django-realtime-playground在Django中实现了一个实时聊天应用,使用了各种实时技术,例如Socket.io。

AppEngine和AppScale都实现了AppEngine Channel API;该API与Google Realtime API不同,后者由googledrive/realtime-playground演示。


0

服务器端推送技术是这里的最佳选择。Comet是(或曾经是)一个热门词汇。

你采取的特定方向在很大程度上取决于你的服务器堆栈以及你/它的灵活性。如果可以的话,我建议看一下socket.io,它提供了WebSockets的跨浏览器实现,这提供了一种非常流畅的方式来与服务器进行双向通信,允许服务器向客户端推送更新。

特别是,请参见该库作者的演示,几乎完全展示了你所描述的情况。


这是一个非常好的库,可以减少通讯问题,但我更希望得到有关如何设计应用程序的高级信息。 - Raynos
1
请注意,socket.io(和SignalR)是使用WebSockets作为首选的框架,但具有兼容的回退功能,可以使用其他技术,如comet、长轮询、Flash Sockets和ForeverFrames。 - Tracker1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接