首页 文章

如何设计一个多用户ajax Web应用程序是同时安全的

提问于
浏览
94

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

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

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

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

当多个用户同时使用该页面时,我有什么选择让页面与服务器保持同步?

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

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

Bounty Edit:

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

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

8 回答

  • 156

    Overview:

    • 简介

    • 服务器架构

    • 客户端架构

    • 更新案例

    • 提交案例

    • 冲突案件

    • 性能和可扩展性

    嗨雷诺斯,

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

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

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

    1.服务器端:

    • 确定一个合理的级别,您可以在其中定义我称之为“原子工件”的内容(页面?页面上的对象?对象内的值?) . 这取决于您的Web服务器,数据库和缓存硬件,用户数量,对象数量等 . 这不是一个简单的决定 .

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

    • 应用程序范围的唯一ID

    • 递增版本ID

    • 用于写访问的锁定机制(可能是互斥)

    • 一个小历史或"changelog"在一个环形缓冲区内(共享内存适用于那些) . 单个键值对可能也可以,但可扩展性较差 . 见http://en.wikipedia.org/wiki/Circular_buffer

    • 能够有效地向连接的用户提供相关更改日志的服务器或伪服务器组件 . 观察者模式是你的朋友 .

    2.客户端:

    • 一个javascript客户端,它能够与上面的服务器进行长时间运行的HTTP连接,或者使用轻量级轮询 .

    • javascript artifact-updater组件,当连接的javascript客户端通知观察的工件历史记录中的更改时,刷新网站内容 . (再次观察者模式可能是一个不错的选择)

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

    • 一个javascript冲突解决器,允许人为改变是正确的决定 . 你可能不想只告诉用户“有人比你快 . 我删除了你的改变 . 去哭吧 . ”很多选择来自相当技术的差异或更加用户友好的解决方案似乎是可能的 .

    那怎么滚...

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

    • 浏览器呈现页面

    • javascript "sees"工件,每个工件至少有一个值字段,unique-和version-id

    • javascript客户端启动,从找到的版本开始请求"watch"找到的工件历史记录变化不是很有趣)

    • 服务器进程记录请求并持续检查和/或发送历史记录

    • 历史条目可能包含简单通知"artifact x has changed, client pls request data",允许客户端独立轮询或完整数据集"artifact x has changed to value foo"

    • javascript artifact-updater尽可能在知道更新后立即获取新值 . 它执行新的ajax请求或由javascript客户端提供 .

    • 页面DOM内容已更新,可选择通知用户 . 历史观察仍在继续 .

    案例2:现在提交:

    • artifact-committer从用户输入中获知所需的新值,并向服务器发送更改请求
      获得

    • serverside mutex

    • 服务器收到"Hey, I know artifact x's state from version 123, let me set it to value foo pls."

    • 如果工件x的Serverside版本等于(不能小于)123,则接受新值,生成新版本ID 124 .

    • 新的状态信息"updated to version 124"和可选的新值foo放在工件x的ringbuffer的开头(changelog / history)

    • serverside mutex已发布

    • 请求工件提交者很高兴收到提交确认和新ID .

    • 同时服务器端服务器组件继续轮询/推送环缓冲区到连接的客户端 . 观察工件x的缓冲区的所有客户端将在其通常的延迟期间获得新的状态信息和值(参见案例1) .

    案例3:冲突:

    • 工件提交者从用户输入中获知所需的新值,并向服务器发送更改请求

    • 同时另一个用户成功更新了相同的工件(参见案例2),但由于各种延迟,我们的其他用户还不知道 .

    • 因此获取了服务器端互斥锁(或等到"faster"用户提交了他的更改)

    • 服务器收到"Hey, I know artifact x's state from version 123, let me set it to value foo."

    • 在Serverside上,工件x的版本现在已经是124 . 请求客户端无法知道他将覆盖的值 .

    • 显然,服务器必须拒绝更改请求(不计入上帝介入的覆盖优先级),释放互斥锁并且非常友好地将新版本ID和新值直接发送回客户端 .

    • 面对被拒绝的提交请求和变更请求用户尚未知道的值,javascript工件提交者指的是冲突解决程序,它向用户显示并解释问题 .

    • 允许用户通过智能冲突解决器JS向其提供一些选项,以便再次尝试更改该值 .

    • 一旦用户选择了他认为正确的值,该过程将从案例2开始(或案例3,如果其他人更快,则再次)

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

    HTTP轮询与HTTP“推送”

    • 轮询创建请求,每秒一次,每秒5次,无论您认为什么是可接受的延迟 . 如果你没有将你的(Apache?)和(php?)配置得足够好,那么这对你的基础设施来说可能是相当残酷的 . -2553821_ starters . 希望在服务器端优化轮询请求,使其运行的时间远小于轮询间隔的长度 . 将运行时分成两半可能意味着将整个系统负载降低多达50%,

    • 通过HTTP推送(假设网络工作者太远而无法支持它们)将要求您为每个用户提供一个apache / lighthttpd进程 all the time . 为每个进程和系统总内存保留的驻留内存将是您将遇到的一个非常确定的扩展限制 . 减少连接的内存占用量是必要的,并且限制每个中的连续CPU和I / O工作量(您需要大量的睡眠/空闲时间)

    后端缩放

    • 忘记数据库和文件系统,你需要某种基于共享内存的后端来进行频繁的轮询(如果客户端不直接轮询那么每个正在运行的服务器进程都会)

    • 如果你去memcache你可以更好地扩展,但它仍然很昂贵

    • 即使你想让多个前端服务器负载均衡,提交的互斥锁也必须全局工作 .

    前端缩放

    • 无论您是轮询还是接收"pushes",都要尝试一步获取所有观察过的工件的信息 .

    “创意”调整

    • 如果客户端正在轮询并且许多用户倾向于观看相同的工件,您可以尝试将这些工件的历史记录发布为静态文件,允许apache缓存它,然后在工件更改时在服务器端刷新它 . 这会将PHP / memcache从游戏中取出一些请求 . Lighthttpd在提供静态文件方面非常有效 .

    • 使用像cotendo.com这样的内容传送网络来推送工件历史记录 . 推送延迟将更大,但可扩展性是一个梦想

    • 编写用户使用java或flash(?)连接的真实服务器(不使用HTTP) . 您必须处理在一个服务器线程中为许多用户提供服务 . 循环通过打开的插座,完成(或委托)所需的工作 . 可以通过分叉过程或启动更多服务器进行扩展 . 互斥体必须保持全球独特 .

    • 根据加载方案,按artifact-id范围对前端服务器和后端服务器进行分组 . 这将允许更好地使用持久性存储器(没有数据库具有所有数据)并且可以扩展静音 . 您的javascript必须同时保持与多个服务器的连接 .

    我希望这可以成为你自己想法的开始 . 我相信还有更多的可能性 . 我非常欢迎任何批评或改进此帖子,维基启用 .

    Christoph Strasen

  • 13

    我知道这是一个老问题,但我想我只是想进来 .

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

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

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

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

  • 1

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

  • 1

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

    将来有一种更简单的方法可以做到这一点,称为WebSockets,但由于存在关于标准当前状态的安全问题,因此尚不确定该标准何时准备好进入黄金时段 .

    使用新对象的数据库中不应存在并发问题 . 但是,当用户编辑对象时,服务器需要具有一些逻辑,以检查对象是否已在此期间被编辑或删除 . 如果该对象已被删除,则解决方案再次简单:只需丢弃编辑 .

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

    第一次真正做到这一点的少数几个实现之一是EtherPad,它被谷歌收购 . 我相信他们然后使用了一些EtherPad _2553829告诉我这一点 . 谷歌也开放了EtherPad,所以也许是's worth a look, depending on what you'试图这样做 .

    由于延迟,它无法在Web上进行原子操作 . 也许this article会帮助你了解更多关于话题 .

  • 0

    试着自己写这一切是一项很重要的工作,而且很难做到这一点 . 一种选择是使用一个框架来构建,以使客户端与数据库保持同步,并实现彼此同步 .

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

    “Meteor采用了反应式编程的概念 . 这意味着您可以用简单的命令式编写代码,并且只要代码所依赖的数据发生变化,结果就会自动重新计算 . ”

    “这种简单的模式(反应计算反应数据源)具有广泛的适用性 . 程序员可以避免编写取消订阅/重新订阅调用,并确保在正确的时间调用它们,从而消除了整个类别的数据传播代码,否则会堵塞您的应用程序容易出错的逻辑 . “

  • 2

    我无法相信没人提到Meteor . 这是一个新的,不成熟的框架肯定(并且只正式支持一个数据库),但它需要所有繁琐的工作和思考,如海报所描述的多用户应用程序 . 事实上,你可以快速总结一下:

    • 所有内容都在node.js(JavaScript或CoffeeScript)中,因此您可以在客户端和服务器之间共享诸如验证之类的内容 .

    • 它使用websockets,但可以退回旧版浏览器

    • 它专注于对本地对象的即时更新(即UI感觉很快),并在后台将更改发送到服务器 . 只允许原子更新使混合更新更简单 . 回滚服务器上拒绝的更新 .

    • 作为奖励,它会为您处理实时代码重新加载,并且即使应用程序发生根本变化,也会保留用户状态 .

    流星很简单,我建议你至少看看它是否有偷窃的想法 .

  • 5

    这些维基百科页面可能有助于增加学习concurrencyconcurrent computing的视角,以便在messaging pattern中设计ajax _255383__或pushed状态eventEDAmessages . 基本上,消息被复制到通道订户,响应更改事件和同步请求 .

    有许多形式的并发基于Web的collaborative software .

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

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

    AppEngine和AppScale都实现AppEngine Channel API;这与Google Realtime API不同,后者由googledrive/realtime-playground证明 .

  • 3

    Server-side push技术是这里的方式 . Comet是(或者是?)一个流行语 .

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

    特别是,请参阅图书馆作者的演示文稿,该演示文稿几乎完全展示了您描述的情况 .

相关问题