96

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

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

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

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

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

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

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

赏金编辑:

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

即同时访问功能和数据库上的数据,没有任何数据或状态损坏的风险

4

8 回答 8

158

概述:

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

嗨雷诺斯,

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

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

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

1.服务器端:

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

  • 对于每个原子工件有:

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

2.客户端:

  • 一个 javascript 客户端,能够与上述服务器建立长期运行的 HTTP 连接,或者使用轻量级轮询。

  • 一个 javascript artifact-updater 组件,当连接的 javascript 客户端通知监视的 artifacts-history 中的更改时刷新站点内容。(再次观察者模式可能是一个不错的选择)

  • 一个 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“推送”

  • 轮询创建请求,每秒 1 个,每秒 5 个,无论您认为什么是可接受的延迟。如果您没有将 (Apache?) 和 (php?) 配置为“轻量级”启动器,那么这对您的基础架构可能是相当残酷的。需要优化服务器端的轮询请求,使其运行时间远少于轮询间隔的长度。将运行时间分成两半可能意味着将整个系统负载降低多达 50%,
  • 通过 HTTP 推送(假设 webworkers 太远无法支持它们)将要求您始终为每个用户提供一个 apache/lighthttpd进程。为这些进程中的每一个保留的常驻内存和您的系统总内存将是您将遇到的一个非常确定的扩展限制。减少连接的内存占用是必要的,并限制在每一个中完成的连续 CPU 和 I/O 工作量(您需要大量的睡眠/空闲时间)

后端缩放

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

前端缩放

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

“创意”调整

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

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

克里斯托夫·斯特拉森

于 2011-02-17T16:32:18.727 回答
13

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

OT(操作转换)似乎非常适合您对并发和一致的多用户编辑的要求。这是一种用于 Google Docs的技术(也用于 Google Wave):

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

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

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

于 2013-07-08T02:04:05.223 回答
5

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

于 2011-02-15T17:23:45.957 回答
3

您需要使用推送技术(也称为 Comet 或反向 Ajax)在对数据库进行更改后立即将更改传播给用户。目前可用的最佳技术似乎是 Ajax 长轮询,但并非所有浏览器都支持它,因此您需要后备。幸运的是,已经有解决方案可以为您处理这个问题。其中包括:orbited.org 和已经提到的 socket.io。

将来会有一种更简单的方法来做到这一点,称为 WebSockets,但目前还不确定该标准何时准备好迎接黄金时段,因为该标准的当前状态存在安全问题。

数据库中不应该有新对象的并发问题。但是当用户编辑对象时,服务器需要有一些逻辑来检查对象是否同时被编辑或删除。如果对象已被删除,解决方案也很简单:放弃编辑。

但是当多个用户同时编辑同一个对象时,就会出现最困难的问题。如果用户 1 和 2 同时开始编辑一个对象,他们都会对相同的数据进行编辑。假设用户 1 所做的更改首先发送到服务器,而用户 2 仍在编辑数据。然后您有两个选择:您可以尝试将用户 1 的更改合并到用户 2 的数据中,或者您可以告诉用户 2 他的数据已过期,并在他的数据发送到服务器后立即向他显示错误消息。后者在这里不是非常用户友好的选项,但前者很难实现。

第一次真正做到这一点的少数几个实现之一是EtherPad,它被 Google 收购了。我相信他们随后在 Google Docs 和 Google Wave 中使用了 EtherPad 的一些技术,但我不能确定这一点。谷歌还开源了 EtherPad,所以也许值得一看,这取决于你想要做什么。

同时编辑东西真的很不容易,因为由于延迟,不可能在网络上进行原子操作。也许本文将帮助您了解有关该主题的更多信息。

于 2011-02-17T00:19:25.700 回答
2

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

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

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

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

于 2013-07-08T04:51:13.387 回答
1

我不敢相信没有人提到Meteor。它肯定是一个新的和不成熟的框架(并且只正式支持一个数据库),但它需要所有繁重的工作和思考海报所描述的多用户应用程序。事实上,您不能不构建多用户实时更新应用程序。这是一个快速的总结:

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

Meteor 很简单,我建议你至少看一下它,以便窃取想法。

于 2013-07-08T03:09:45.377 回答
1

这些 Wikipedia 页面可能有助于增加了解并发性并发计算的视角,以设计一个ajax Web 应用程序,该应用程序以消息传递模式提取推送状态事件( EDA )消息。基本上,消息被复制到响应更改事件和同步请求的通道订阅者。

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

etherpad-lite有许多HTTP API 客户端库,一个协作式实时编辑器

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

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

于 2013-07-08T05:55:49.723 回答
0

服务器端推送技术是这里的必经之路。彗星是(或曾经是?)一个流行词。

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

特别是,请参阅库作者的这个演示,它几乎完全展示了您描述的情况。

于 2011-02-15T18:34:23.747 回答