如何设计一个多用户 ajax Web 应用程序以保证并发安全

Posted

技术标签:

【中文标题】如何设计一个多用户 ajax Web 应用程序以保证并发安全【英文标题】:How to design a multi-user ajax web application to be concurrently safe 【发布时间】:2011-06-16 11:01:00 【问题描述】:

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

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

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

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

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

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

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

赏金编辑:

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

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

【问题讨论】:

不太确定,但您可以拥有像 facebook 这样的页面,其中浏览器发送 ajax 请求不断寻求服务器数据库中的更改并在浏览器上更新它们 序列化客户端状态,然后通过 ajax 告诉服务器这是我的状态,我需要更新什么是一个选项。但要求客户知道如何在一个地方更新任何和每一点信息。 用户端并发的最佳解决方案不只是推送变体之一吗? Websockets、comet 等。 @davin 很可能是这样。但我对 Comet 不熟悉,而且 websockets 不支持跨浏览器。 有很好的跨浏览器支持包,我特别推荐socket.io,虽然也有jWebSocket等。如果你采用 socket.io 的方式,你可以合并各种 node.js 好东西,比如框架和(客户端)模板引擎等。 【参考方案1】:

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

【讨论】:

这点很有用。它还有助于我从设计角度更多地理解我们数据库中的“LastEdited”字段。 没错。该站点使用“心跳”,这意味着它每 x 次向服务器发送一个 AJAX 请求,并传递要检查的数据 ID,以及它对该数据的修改时间戳。假设我们正在处理问题#1029。每个 AJAX 请求,服务器只查看问题 #1029 的修改时间戳。如果它发现客户端有旧版本的数据,它会用新副本回复心跳。然后,客户端可以重新加载页面(刷新),或者向用户显示某种消息,警告他们有新数据。 修改后的邮票比对我们当前的“数据”进行散列并将其与另一侧的散列进行比较要好得多。 请记住,客户端和服务器必须能够访问完全相同的时间以避免不一致。【参考方案2】:

Server-side push 技术是通往这里的道路。 Comet 是(或者曾经是?)一个流行词。

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

特别是,请参阅库作者的 this 演示,该演示几乎完全符合您描述的情况。

【讨论】:

这是一个很好的库,可以减少合并问题,但我更多的是寻找有关如何设计应用程序的高级信息 请注意,socket.io(和 SignalR)是使用 websockets 作为一流选择的框架,但具有兼容的回退以使用其他技术,如彗星、长轮询、闪存套接字和永远帧。【参考方案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,所以也许值得一看,具体取决于您要做什么。

同时编辑内容确实不容易,因为由于延迟,不可能在网络上进行原子操作。也许this article 会帮助您了解更多有关该主题的信息。

【讨论】:

【参考方案4】:

概述:

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

嗨雷诺斯,

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

从架构的角度来看,您似乎遇到了与版本控制软件相同的问题。一个用户签入对对象的更改,另一个用户想以另一种方式更改同一对象=> 冲突。您必须整合用户对对象的更改,同时能够及时有效地提供更新,检测和解决上述冲突。

如果我处于你的位置,我会开发这样的东西:

1。服务器端:

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

对于每个原子工件有:

应用程序范围的唯一 ID 递增的版本 ID 写访问的锁定机制(可能是互斥锁) 环形缓冲区内的小历史记录或“更改日志”(共享内存适用于这些)。单个键值对也可能没问题,但可扩展性较差。见http://en.wikipedia.org/wiki/Circular_buffer

能够有效地将相关变更日志传递给连接用户的服务器或伪服务器组件。观察者模式是你的朋友。

2。客户端:

能够与上述服务器建立长时间运行的 HTTP-Connection 或使用轻量轮询的 javascript 客户端。

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

一个 javascript artifact-committer 组件,它可能请求更改原子工件,试图获取互斥锁。它将通过比较已知的客户端 artifact-version-id 和当前的服务器端 artifact-version-id 来检测工件的状态是否在几秒钟前被另一个用户更改(javascript 客户端的延迟和提交过程因素)。

一个 javascript 冲突解决程序,允许人类做出正确的决定。您可能不想只告诉用户“有人比您快。我删除了您的更改。去哭吧。”。来自相当技术差异或更用户友好的解决方案的许多选项似乎是可能的。

那么它会如何滚动......

案例1:更新的序列图种类:

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

案例 2:现在提交:

artifact-committer 从用户输入中知道所需的新值并向服务器发送更改请求 获取服务器端互斥锁 服务器收到“嘿,我从版本 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 距离太远而无法支持它们)将要求您为每个用户始终拥有一个可用的 apache/lighthttpd 进程。为这些进程中的每一个保留的常驻内存和您的系统总内存将是您将遇到的一个非常确定的扩展限制。减少连接的内存占用是必要的,并且限制在每个连接中完成的连续 CPU 和 I/O 工作量(您需要大量的睡眠/空闲时间)

后端缩放

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

前端缩放

无论您是轮询还是接收“推送”,都请尝试在一个步骤中获取所有已观看工件的信息。

“创意”调整

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

我希望这可以成为您自己想法的开始。我相信还有更多的可能性。 我非常欢迎对这篇文章的任何批评或改进,wiki 已启用。

克里斯托夫·斯特拉森

【讨论】:

@ChristophStrasen 看看像 node.js 这样的事件服务器,它不依赖每个用户一个线程。这允许以较低的内存消耗来处理推送技术。我认为 node.js 服务器和依赖于 TCP WebSockets 确实有助于扩展。不过,它完全破坏了跨浏览器的合规性。 我完全同意并希望我的文章不会鼓励重新发明***!虽然有些***有点新,但刚刚开始为人所知,并且解释得不够好,因此中级软件架构师可以判断它的应用是否有特定的想法。恕我直言。 Node.js 有点值得一本“傻瓜”的书;)。我肯定会买。 +500 你已经挑衅了这一本。这是一个很好的答案。 @luqmaan 这个答案来自 2011 年 2 月。Websockets 仍然是一个新事物,并且仅在 8 月左右在 Chrome 中发布了无前缀。 Comet 和 socket.io 很好,但我认为这只是对更高性能方法的建议。 如果 Node.js 离您的舒适区太远了(或运营团队的舒适区,但确定问题的业务上下文),您也可以使用 Socket.io使用基于 Java 的服务器。 Tomcat 和 Jetty 都支持用于服务器推送类型的设置的无线程连接(参见例如:wiki.eclipse.org/Jetty/Feature/Continuations)【参考方案5】:

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

OT (operational transforms) 似乎非常适合您对并发和一致的多用户编辑的要求。这是一个technique used in Google Docs(也用于 Google Wave):

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

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

它使用 BrowserChannel 进行服务器和客户端之间的通信(我相信 WebSockets 支持应该在工作中 - 它以前通过 Socket.IO 在那里,但由于开发人员与 Socket.io 的问题而被删除)初学者不过,目前文档有点少。

【讨论】:

【参考方案6】:

我不敢相信没有人提到Meteor。它肯定是一个新的和不成熟的框架(并且仅正式支持一个 DB),但它需要所有繁重的工作,并像海报所描述的那样从多用户应用程序中思考。事实上,您不能不构建多用户实时更新应用程序。下面是一个简短的总结:

一切都在 node.js(JavaScript 或 CoffeeScript)中,因此您可以在客户端和服务器之间共享验证等内容。 它使用 websocket,但可以回退到旧版浏览器 它专注于对本地对象的即时更新(即 UI 感觉很流畅),并在后台将更改发送到服务器。只允许原子更新以使混合更新更简单。在服务器上被拒绝的更新被回滚。 作为奖励,它可以为您处理实时代码重新加载,并且即使在应用发生根本变化时也会保留用户状态。

Meteor 很简单,我建议你至少看看它,以获取灵感。

【讨论】:

我真的很喜欢 Derby 和 Meteor 用于某些类型的应用程序的想法。文档/记录的所有权和权限只是一些现实世界的问题,恕我直言。此外,由于长期以来 MS 的世界让这 80% 变得非常容易,而在另外 20% 上花费了太多时间,我对使用这种 PFM(纯他妈的魔法)解决方案犹豫不决。【参考方案7】:

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

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

“Meteor 包含反应式编程的概念。这意味着您可以以简单的命令式风格编写代码,并且每当您的代码所依赖的数据发生变化时,结果将自动重新计算。”

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

【讨论】:

【参考方案8】:

这些***页面可能有助于增加了解 concurrency 和 concurrent computing 的视角,以设计 ajax web application 或者 pulls 或者是 pushed 状态 event (EDA) @987654329 @ 在messaging pattern 中。基本上,消息被复制到通道订阅者,这些订阅者响应更改事件和同步请求。

https://en.wikipedia.org/wiki/Category:Concurrency_control https://en.wikipedia.org/wiki/Distributed_concurrency_control https://en.wikipedia.org/wiki/CAP_theorem https://en.wikipedia.org/wiki/Operational_transformation https://en.wikipedia.org/wiki/Fallacies_of_Distributed_Computing

基于web的并发collaborative software有多种形式。

有多个HTTP API client libraries for etherpad-lite,一个collaborative real-time editor。

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

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

【讨论】:

以上是关于如何设计一个多用户 ajax Web 应用程序以保证并发安全的主要内容,如果未能解决你的问题,请参考以下文章

如何对 Zend Web 应用程序和 Zend JSON-RPC 服务器使用相同的“后端”代码?

JSP之Ajax

Web前端基础——Ajax

多客户端Web应用程序设计

WEB通用抽奖程序设计多情景下的用户模式

Jmeter - 如何使用缓存加载多个用户的测试 ajax Web 应用程序