处理并发问题的最佳方法

时间:2009-11-22 17:17:51

标签: php mysql postgresql concurrency

我有一个LAPP(linux,apache,postgresql和php)环境,但问题在Postgres或Mysql上都是一样的。

我开发了一个cms应用程序,用于处理客户端,文档(估计,发票等)和其他数据,在1个postgres DB中构建,包含许多模式(每个客户使用该应用程序一个);让我们假设大约200个模式,每个模式同时由15个人使用(平均)。

编辑:我在每个表上都有一个名为last_update的时间戳字段,以及每次更新行时更新时间戳的触发器。

情况是:

  1. People Foo和Bar正在编辑文档0001,使用包含每个文档详细信息的表单。
  2. Foo更改货件详细信息,例如。
  3. 栏会更改电话号码以及文档中的某些项目。
  4. Foo按“保存”按钮,应用程序更新数据库。
  5. 栏栏后按“保存”按钮,重新发送包含旧货件详情的表格。
  6. 在数据库中,Foo更改已丢失。
  7. 我想要的情况:

    1. People Foo,Bar,John,Mary,Paoul正在编辑文档0001,使用包含每个文档详细信息的表单。
    2. Foo更改货件详细信息,例如。
    3. Bar和其他人改变了别的东西。
    4. Foo按“保存”按钮,应用程序更新数据库。
    5. Bar和其他人收到警报'警告!这个文件已被其他人改变了。点击此处加载实际数据'。
    6. 我想知道用ajax来做这件事;只需使用带有文档ID和最后更新时间戳的隐藏字段,每隔5秒检查最后更新时间是否相同,什么都不做,否则,显示警告对话框。

      因此,页面check-last-update.php应该类似于:

      <?php
      //[connect to db, postgres or mysql]
      $documentId = isset($_POST['document-id']) ? $_POST['document-id'] : 0;
      $lastUpdateTime = isset($_POST['last-update-time']) ? $_POST['last-update-time'] : 0;
      //in the real life i sanitize the data and use prepared statements;
      $qr = pg_query("
          SELECT
              last_update_time
          FROM
              documents
          WHERE
              id = '$documentId'
      ");
      $ray = pg_fetch_assoc($qr);
      if($ray['last_update_time'] > $lastUpdateTime){
          //someone else updated the document since i opened it!
          echo 'reload';
      }else{
          echo 'ok';
      }
      ?>
      

      但我不想每隔5秒对每个打开一个(或多个......)文档的用户强调数据库。

      那么,在没有核心数据库的情况下,还有什么可以成为另一个高效解决方案?

      我想使用文件,例如为每个文档创建一个空的txt文件,每次更新文档时,我都会“触摸”更新“上次修改时间”的文件...但我猜这个当我有很多用户编辑同一个文档时,会比db慢,并且会出现问题。

      如果其他人有更好的想法或任何建议,请详细说明!

      * - - - - - 更新 - - - - - *

      我肯定选择不点击数据库来检查'上次更新时间戳',不介意查询是否会非常快,(主)数据库服务器还有其他任务要填满,不喜欢增加超载的想法那件事。

      所以,我采取这种方式:

      1. 每次某人更新文档时,我必须做一些事情来在数据库环境之外签署新的时间戳,例如:没有问db。我的想法是:
        1. 文件系统:对于每个文档,我创建一个名为文档ID的empry txt文件,每次文档更新时,我都会触摸该文件。我希望有数千个空文件。
        2. APC,php cache:这可能比第一个更灵活,但是我想知道在apc中永久保存成千上万的数据是不会减慢php执行本身,或者消耗服务器内存。我有点害怕选择这种方式。
        3. 另一个db,sqlite或mysql(使用简单的数据库结构更快更轻)用于存储文档ID和时间戳。
      2. 无论我选择哪种方式(文件,apc,sub-db)我都认真考虑在子域上使用另一个网络服务器(lighttp?)来处理所有那些...长轮询请求。
      3. 另一个编辑:

        文件的方式不起作用。

        APC可以成为解决方案。

        点击数据库也可以成为解决方案,创建一个表来处理时间戳(只有两个列,document_id和last_update_timestamp),这些表需要尽可能快速和轻便。

        长轮询:这是我选择的方式,在apache下使用lighttpd加载静态文件(图像,css,js等等),并且仅用于这种类型的长轮询;这将减轻apache2负载,特别是对于轮询。

        Apache会将所有这些请求代理到lighttpd。

        现在,我只需要在db解决方案和APC解决方案之间做出决定..

        p.s:感谢所有已经回答我的人,你真的很有用!

11 个答案:

答案 0 :(得分:3)

我同意我可能不会为此打到数据库。我想我会使用APC缓存(或其他一些内存缓存)来维护这些信息。您所描述的内容显然是乐观锁定在详细记录级别。数据库结构中的级别越高,您需要处理的越少。听起来你想要检查一个结构中的多个表。

我将维护ID的缓存(在APC中)以及由表名键控的上次更新时间的时间戳。因此,例如,我可能有一个表名数组,其中每个条目都按ID键入,实际值是最后更新的时间戳。有很多方法可以通过数组或其他结构进行设置,但是你可以理解。我可能会在缓存中添加超时,以便在一段时间后删除缓存中的条目 - 即,我不希望缓存增长并假设1天的条目不再有用。 / p>

使用此架构,您需要执行以下操作(除了设置APC):

  • 对任何(适用)表的任何更新,使用新时间戳更新APC缓存条目。

  • ajax中的
  • 只是像php一样“返回”(获取APC缓存来检查条目),而不是一直“回”到数据库。

答案 1 :(得分:2)

我认为您可以在UPDATE语句中使用条件,如WHERE ID =? AND LAST_UPDATE =?。

这个想法是,只有当你是读取该行的最后一个时,才能成功更新。如果其他人提交了某些内容,您将失败,一旦您知道自己失败了,就可以查询更改。

答案 2 :(得分:1)

每条记录都需要某种类型的版本标记字段。只要您能保证对记录进行任何更改都会导致该版本标记不同,那么它无关紧要。最佳做法是检查并确保加载记录的版本标记与用户单击保存时数据库中的版本标记相同,如果不同则处理它。

你如何处理它取决于你。至少你想提供从数据库重新加载,以便用户可以验证他们仍然想要保存。其中一个是尝试将他们的更改合并到新的DB记录中,然后让他们验证合并是否正常工作。

如果您想定期轮询任何能够处理您的系统的数据库应该能够进行轮询加载。每5秒轮询一次的10个用户是每秒2个事务。这是一个微不足道的负担,应该没有任何问题。为了保持平均负载接近实际负载,只需略微抖动轮询时间(而不是每5秒完成一次,例如每隔4-6秒执行一次)。

答案 3 :(得分:1)

Donnie的回答(民意调查)可能是你最好的选择 - 简单而有效。它几乎涵盖了所有情况(即使在非常受欢迎的网站上,简单的PK查找也不会影响性能)。

为了完整性,如果您想避免轮询,可以使用push-model。维基百科文章中描述了各种方法。如果您可以维护一个直写缓存(每次更新记录,更新缓存),那么几乎可以完全消除数据库负载。

不要使用时间戳“last_updated”列。在同一秒内编辑并非闻所未闻。如果您添加额外信息(执行更新的服务器,远程地址,端口等),您可以逃脱它,以确保如果两个请求在同一秒内进入同一服务器,您可以检测到差异。但是,如果您需要这种精度,您也可以使用唯一的修订字段(它不一定是递增的整数,在该记录的生命周期内是唯一的)。

有人提到持久连接 - 这会降低轮询查询的设置成本(每个连接自然会消耗数据库和主机上的资源)。您将保持一个连接(或尽可能少)连接(或尽可能长)并使用它(如果需要,与缓存和memoization结合使用)。

最后,有一些SQL语句允许您在UPDATE或INSERT上添加条件。我的SQl真的很生锈,但我觉得它像UPDATE ... WHERE ...。要匹配此级别的保护,您必须在发送更新之前执行自己的行锁定(以及可能需要的所有错误处理和清理)。你不太可能需要这个;我只是提到它的完整性。

修改

您的解决方案听起来不错(缓存时间戳,代理轮询请求到另一台服务器)。我做的唯一更改是更新每次保存的缓存时间戳。这将使缓存更新鲜。我还会在保存时直接从数据库中检查时间戳,以防止由于过时的缓存数据而导致保存隐藏。

如果使用APC进行缓存,则第二个HTTP服务器没有意义 - 您必须在同一台计算机上运行它(APC使用共享内存)。相同的物理机器将完成工作,但需要额外的第二个HTTP服务器开销。如果您想将轮询请求卸载到第二台服务器(在您的情况下为lighttpd),那么最好在第二台物理计算机上的Apache前面设置lightttpd并使用共享缓存服务器(memcache)以便lighttpd服务器可以读取缓存的时间戳,Apache可以更新缓存的时间戳。如果大多数请求都是轮询请求,那么将lighttpd放在Apache前面的理由就是避免使用更重的Apache进程。

你可能根本不需要第二台服务器。 Apache应该能够处理其他请求。如果它不能,那么我将重新访问您的配置(特别是控制您运行的工作进程数量以及在被杀之前允许处理的请求数量的指令)。

答案 4 :(得分:0)

您查询数据库的方法是最好的。如果你每5秒做一次并且你有15个并发用户,那么你每秒查看~3个查询。它应该是一个非常小的查询,只返回一行数据。如果您的数据库每秒无法处理3个事务,那么您可能需要查看更好的数据库,因为3个查询/秒不算什么。

为表中的记录添加时间戳,以便您可以快速查看是否有任何更改,而无需区分每个字段。

答案 5 :(得分:0)

Hibernate使用版本字段来做到这一点。为每个表提供这样的字段,并使用触发器在每次更新时递增它。存储更新时,将当前版本与先前读取数据时的版本进行比较。如果那些不匹配,则抛出异常。使用事务来进行检查和更新原子。

答案 6 :(得分:0)

这稍微偏离了主题,但你可以使用PEAR包(或PECL包,我忘了哪个)xdiff在你发生碰撞时发回良好的用户指导。

答案 7 :(得分:0)

首先,只更新写入数据库时​​已更改的字段,这将减少数据库负载。

其次,查询上次更新的时间戳,如果您有较旧的时间戳,则数据库中的当前版本会向客户端发出警告。

第三是以某种方式将此信息推送到客户端,尽管与服务器建立了某种持久连接,从而实现了并发双向连接。

答案 8 :(得分:0)

轮询很少是一个很好的解决方案 只有当用户(使用打开的文档)正在对文档执行某些操作(如滚动,将鼠标移到其上或开始编辑)时,才可以执行timstamp检查。然后,如果文档已被更改,则用户会收到警报
.....
我知道这不是你要求的但是......为什么不是编辑单身? 单例可以是文档表中的userID列 如果用户想要编辑文档,则会锁定该文档以供其他用户编辑。

或者在各个字段/信息组上有编辑单例。

一次只能有一个用户编辑文档。如果另一个用户打开了文档并想要编辑单个时间戳检查,则表明文档已被更改并重新加载。

使用单例时,没有轮询,只有一个时间戳检查用户“触摸”和/或想要编辑文档。

但也许单身机制不适合你的系统
问候
Sigersted

答案 9 :(得分:0)

啊,我虽然这很容易。

所以,让我们说明一点:我有一个通用数据库(pgsql或mysql并不重要),它包含许多通用对象。

我有$ x(实际上是$ x = 200,但正在增长,希望很快会达到1000)这个数据库的精确副本,并且每天最多20​​个(平均10)用户每天9小时。

如果其中一个用户正在查看记录,任何记录,如果有人编辑同一记录,我必须建议他。

让我们说Foo正在观看文件0001,坐下来喝咖啡,Bar打开并编辑同一个文件,当Foo回来时他必须看到'警告,其他人编辑了这个文件!点击这里刷新页面。'。

那么我需要atm,可能我会扩展这种情况,添加一种方法来查看更改和回滚,但这不是重点。

有些人建议仅在foo尝试保存文档时检查“上次更新”时间戳;也可以是一个解决方案,但我需要实时(10秒深)。

漫长的民意调查,糟糕的方式,但似乎是唯一的一个。

所以,我做了什么:

  1. 在我的机器上安装Lighttp(和php5作为fastcgi);
  2. 加载apache2的代理模块(全部,或403错误会打击你);
  3. 将lighttpd端口从80(由apache2使用)更改为81;
  4. 配置apache2以将请求从mydomain.com/polling/*代理到polling.mydomain.com(与Lighttp一起提供)
  5. 现在,我有另一个子http服务,我将用它来轮询和加载静态内容(图像等等),以减少apache2的负载。
  6. 因为我不想核数据库进行时间戳检查,我尝试了一些缓存系统(可以从php调用)。
    1. APC:安装和管理非常简单,非常轻巧,速度更快,这将是我的第一选择..如果只有缓存可以在两个cgi进程之间共享(我需要在缓存中存储来自apache2的php进程的值,并从lighttpd的php进程中读取它)
    2. Memcached:比APC慢大约4-5倍,但作为一个单一的进程运行,可以触及我环境中的任何地方。 我会选择这个,atm。(即使速度较慢,使用它也会相对简单)。
  7. 现在,我只需要尝试这个系统加载一些测试数据,看看ho会在“压力下”移动并优化它。

    我认为这种环境适用于其他长轮询情况(聊天?)

    感谢所有给我听到的人!

答案 10 :(得分:0)

我建议:当您第一次查询可能更改的记录时,请挂起本地副本。 “更新”时,将锁定的表格/行中的副本与您的副本进行比较,如果已更改,请将其重新发送给用户。