如何设计多用户ajax Web应用程序以使其并发安全


95

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

每次用户交互并更改此数据(说用户A重命名)时,它都会告诉服务器执行操作,然后服务器返回新的更改数据。

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

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

当多个用户同时使用该页面时,我有哪些选项可以使该页面与服务器保持同步?

宁可避免每次更改时锁定整个页面或将整个状态转储给用户的选项。

如果有帮助,则在此特定示例中,网页将调用静态Web方法,该方法在数据库上运行存储过程。存储过程将返回它已更改的所有数据,并且不再返回。然后,静态Web方法将存储过程的返回转发给客户端。

赏金编辑:

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

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


不太确定,但是您可以拥有Facebook之类的页面,其中浏览器发送ajax请求,不断寻找服务器数据库中的更改并在浏览器中进行更新
Santosh Linkha,2011年

序列化客户端状态,然后通过ajax告诉服务器,这是我的状态,我需要更新什么是一种选择。但是要求客户知道如何在一处更新任何信息。
雷诺斯2011年

1
用户端并发的最佳解决方案不仅是push变体之一吗?网络套接字,彗星等
戴文2011年

@davin可能会的。但是我不熟悉Comet,并且没有Websocket支持跨浏览器。
雷诺斯

2
有很好的跨浏览器支持程序包,特别是我推荐使用socket.io,尽管也有jWebSocket等等。如果你去socket.io方式,你可以将各种node.js的好东西,像框架和(客户端)的模板引擎等等
达文

Answers:


157

概述:

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

嗨,雷诺斯,

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

从体系结构的角度来看,您似乎遇到了与版本控制软件相同的问题。一个用户签入对对象的更改,另一用户想要以另一种方式更改同一对象=>冲突。您必须将用户的更改集成到对象中,同时又要能够及时有效地交付更新,以检测和解决上述冲突。

如果我在你的鞋子里,我会发展出这样的东西:

1.服务器端:

  • 确定一个合理的级别,在该级别上定义我称之为“原子工件”的内容(页面?页面上的对象?对象内的值?)。这将取决于您的Web服务器,数据库和缓存硬件,用户数量,对象数量等。做出一个不容易的决定。

  • 对于每个原子工件都有:

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

2.客户端:

  • 一个能够与上述服务器进行长时间HTTP连接或使用轻量级轮询的javascript客户端。

  • 一个JavaScript工件更新程序组件,当连接的javascript客户端通知观看的工件历史记录中的更改时,该组件会刷新站点内容。(同样,观察者模式可能是一个不错的选择)

  • 一个JavaScript人为因素提交程序组件,可能会请求更改原子人为因素,以尝试获取互斥锁。它将通过比较已知的客户端工件版本ID和当前服务器端工件版本ID来检测工件状态是否在几秒钟前被其他用户更改(JavaScript客户端的等待时间和提交处理因素)。

  • 一个Javascript冲突解决程序,允许人工做出正确的决定。您可能不希望只是告诉用户“有人比您快。我删除了您的更改。哭了。”。从技术差异或更人性化的解决方案中可以找到许多选择。

那么它将如何滚动...

情况1:用于更新的序列图:

  • 浏览器渲染页面
  • javascript“看到”工件,每个工件至少具有一个值字段,唯一标识和版本标识
  • javascript客户端开始使用,请求从其找到的版本开始“观察”找到的工件历史记录(较早的更改并不有趣)
  • 服务器进程记录请求并不断检查和/或发送历史记录
  • 历史记录条目可能包含简单的通知“工件x已更改,客户端请请求数据”,允许客户端独立轮询或完整数据集“工件x已更改为值foo”
  • javascriptartifact-updater会尽力获取已知的更新后的新值。它执行新的ajax请求或由javascript客户端提供。
  • 页面DOM内容已更新,可以选择通知用户。观察历史继续。

情况2:现在提交:

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

情况3:发生冲突:

  • 工件提交者从用户输入中知道所需的新值,并将更改请求发送到服务器
  • 同时,另一个用户成功更新了相同的工件(请参阅案例2),但是由于各种延迟,这对于我们的其他用户而言还是未知的。
  • 因此,获取了服务器端互斥锁(或等待,直到“更快”的用户提交了更改)
  • 服务器收到“嘿,我知道版本123中工件x的状态,让我将其设置为值foo。”
  • 在服务器端,工件x的版本现在已经是124。发出请求的客户端无法知道他将要覆盖的值。
  • 显然,服务器必须拒绝更改请求(不算在干预中间的覆盖优先级中),释放互斥锁,并且足以将新的版本ID和新值直接发送回客户端。
  • 面对被拒绝的提交请求和请求更改的用户尚不知道的值,javascript构件提交程序是指冲突解决程序,它向用户显示并解释问题。
  • 智能冲突解决程序JS向用户提供了一些选项,允许用户再次尝试更改该值。
  • 一旦用户选择了他认为正确的值,该过程就会从情况2(如果其他人更快的话,则从情况3)重新开始。

关于性能和可伸缩性的一些话

HTTP轮询与HTTP“推送”

  • 轮询可以每秒创建一个请求,每秒创建5个请求,无论您认为可接受的延迟如何。如果您没有很好地配置(Apache?)和(php?)以使其成为“轻量级”启动器,那么这对您的基础结构可能会非常残酷。希望在服务器端优化轮询请求,以使其运行时间比轮询间隔的长度短得多。将运行时间减半可能意味着将整个系统的负载降低多达50%,
  • 通过HTTP推送(假设webworkers太过离谱,以支持他们)会要求你有一个阿帕奇/ lighthttpd处理每个用户可用的所有时间。为每个进程保留的常驻内存以及系统总内存将是您将遇到的一个非常确定的扩展限制。减少连接的内存占用将是必要的,并且限制在每个连接中完成的CPU和I / O连续工作量(您需要大量的睡眠/空闲时间)

后端缩放

  • 忘记数据库和文件系统,您将需要某种基于共享内存的后端来进行频繁轮询(如果客户端不直接轮询,则每个正在运行的服务器进程都将进行轮询)
  • 如果您选择内存缓存,可以更好地扩展,但是它仍然很昂贵
  • 即使您希望有多个前端服务器来进行负载平衡,提交的互斥锁也必须全局运行。

前端扩展

  • 无论您是轮询还是接收“推送”,都应尝试一步获得所有已观察工件的信息。

“创意”调整

  • 如果客户端正在轮询并且许多用户倾向于观看相同的工件,则可以尝试将这些工件的历史记录发布为静态文件,允许apache对其进行缓存,但是当工件发生更改时在服务器端刷新它。这使PHP / memcache脱离了一些请求。Lighthttpd在提供静态文件方面非常有效。
  • 使用诸如cotendo.com之类的内容交付网络在此处推送工件历史记录。推送延迟会更大,但可伸缩性是一个梦想
  • 编写用户使用java或flash(?)连接到的真实服务器(不使用HTTP)。您必须处理在一个服务器线程中为许多用户提供服务的情况。通过开放的套接字循环执行(或委派)所需的工作。可以通过分支过程或启动更多服务器来扩展。互斥体必须保持全局唯一性。
  • 根据负载方案,将您的前端服务器和后端服务器按工件ID范围进行分组。这样可以更好地利用持久性内存(没有数据库拥有所有数据),并可以扩展静音。但是,您的JavaScript必须同时保持与多个服务器的连接。

好吧,我希望这可以成为您自己想法的起点。我相信还有很多可能性。我非常欢迎对这篇文章提出任何批评或改进,启用了Wiki。

克里斯多夫·斯特拉森


1
@ChristophStrasen查看事件服务器,例如node.js,它们不依赖于每个用户一个线程。这允许以较低的内存消耗来处理推送技术。我认为node.js服务器和依赖TCP WebSockets确实有助于扩展。但是,它完全破坏了跨浏览器的合规性。
雷诺斯2011年

我完全同意并希望我的文章不会鼓励重新发明轮子!尽管有些轮子有点新,但才刚刚开始为人们所熟知,并且对其解释还不够充分,因此中级软件架构师可以根据特定想法来判断其应用。恕我直言。Node.js还值得一本“傻瓜书”;)。我一定会买。
Christoph Strasen

2
+500您已经勇敢地做到了这一点。这是一个很好的答案。
雷诺斯

1
@luqmaan这个答案是从2011年2月开始的。Websockets仍然是一个新奇事物,并且仅在8月左右以无前缀的形式在Chrome中发布。尽管Comet和socket.io很好,但我认为这只是建议使用一种更高性能的方法。
里卡多·托马西

1
而且,如果Node.js距离您的舒适区(或运营团队的舒适区,但要确定问题的业务范围)有点太远,您也可以将Socket.io与基于Java的服务器一起使用。对于服务器推送样设置的Tomcat和Jetty的支持线程较少的连接(例如参见:wiki.eclipse.org/Jetty/Feature/Continuations
托马斯

13

我知道这是一个古老的问题,但我想我只是插话。

OT(操作转换)似乎非常适合您进行并发且一致的多用户编辑的需求。这是Google文档中使用的一种技术(也曾在Google Wave中使用过):

Google Wave小组的成员编写了一个基于JS的库,用于使用Operational Transforms-ShareJS(http://sharejs.org/)。

而且,如果您愿意,这里有一个完整的MVC Web框架-基于ShareJS的DerbyJS(http://derbyjs.com/)为您完成了所有工作。

它使用BrowserChannel在服务器和客户端之间进行通信(而且我相信WebSockets支持应该已经在起作用-以前是通过Socket.IO提供的,但是由于开发人员与Socket.io的关系而被淘汰了)。但是,目前有点稀疏。


5

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


这是有用的一点。它还从设计的角度帮助我更多地了解数据库中的“ LastEdited”字段。
雷诺斯

究竟。该站点使用“心跳”,这意味着它每x时间向服务器发送AJAX请求,并传递要检查的数据ID以及该数据的修改时间戳。假设我们在问题#1029上。对于每个AJAX请求,服务器仅查看问题#1029的修改后的时间戳。如果发现客户端具有较旧的数据版本,它将使用新副本答复该监听信号。然后,客户端可以重新加载页面(刷新),或向用户显示某种消息,警告他们有关新数据的信息。
克里斯·贝克

修改后的邮票比散列我们当前的“数据”并将其与另一面的散列进行比较要好得多。
雷诺斯

1
请记住,客户端和服务器必须具有完全相同的访问时间,以免出现不一致的情况。
祈祷者

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上执行原子操作。也许本文将帮助您了解有关该主题的更多信息。


2

尝试自己编写所有这些内容是一项艰巨的工作,并且很难正确完成。一种选择是使用一种建立的框架,该框架可以使客户端与数据库以及彼此实时保持同步。

我发现Meteor框架做得很好(http://docs.meteor.com/#reactivity)。

“ Meteor包含了反应式编程的概念。这意味着您可以以简单的命令式风格编写代码,并且只要代码所依赖的数据更改,结果就会自动重新计算。”

“这种简单的模式(响应式计算+响应式数据源)具有广泛的适用性。程序员不必编写取消订阅/重新订阅的调用,并确保在正确的时间调用它们,从而避免了整个类的数据传播代码,否则这些代码会阻塞您的具有易错逻辑的应用程序。”


1

我简直不敢相信没有人提到过流星。当然,这是一个新的,不成熟的框架(并且仅正式支持一个DB),但是它却使所有艰巨的工作和思考都脱离了多用户应用程序(如海报所描述的)。实际上,您不能构建多用户实时更新应用程序。快速摘要:

  • 一切都在node.js(JavaScript或CoffeeScript)中,因此您可以在客户端和服务器之间共享诸如验证之类的东西。
  • 它使用websocket,但可能会回退到旧版浏览器
  • 它着重于对本地对象的立即更新(即,UI感觉很敏捷),并将更改发送到后台。只允许原子更新使混合更新更简单。服务器上拒绝的更新将回滚。
  • 另外,它可以为您处理实时代码重新加载,即使应用程序发生重大变化,它也可以保留用户状态。

流星很简单,我建议您至少看看一下它以窃取想法。


1
对于某些类型的应用程序,我真的很喜欢Derby和Meteor的想法。文档/记录的所有权和权限只是现实世界中的几个问题,尽管不能解决。同样,由于长期的MS世界使80%变得非常容易,而在其他20%上花费了太多时间,我犹豫是否要使用这种PFM(纯粹的魔术)解决方案。
Tracker1

1

这些Wikipedia页面可能有助于为了解有关并发并发计算的观点,以设计ajax Web应用程序,该应用程序消息传递模式拉入推入状态事件EDA消息。基本上,消息被复制到通道订户,该订户响应更改事件和同步请求。

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

还有一些用于EtherPad的-精简版API客户端库HTTP,一个协作的实时编辑

django-realtime-playground使用Socket.io等各种实时技术在Django中实现了实时聊天应用程序。

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


0

服务器端推送技术是解决问题的方法。彗星是(或曾经是)一个时髦的词。

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

特别是看到这个演示由库的作者,这表明几乎完全您描述的情况。


那是一个很好的库,可以减少通信问题,但我更多的是在寻找有关如何设计应用程序的高级信息
Raynos 2011年

1
只是要注意,socket.io(和SignalR)是使用websockets作为首选的框架,但是具有兼容的后备以使用其他技术,例如彗星,长轮询,flash套接字和foreverframes。
Tracker1 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.