【问题标题】:Rollback the transaction when user abort the connection当用户中止连接时回滚事务
【发布时间】:2013-01-30 23:16:32
【问题描述】:

首先,对不起我的英语,我在谷歌翻译的帮助下写了这篇文章。

我正在尝试使用 PHP 和 Ajax 制作像 Google Wave 这样的应用程序。我有一个 textarea,当用户输入内容时,页面上的 javascript 检测到 oninput 并将 textarea 的内容发送到服务器,服务器将内容存储到数据库中。

我正在做的是,每次我通过 XHR 发送内容时,都有XHR.abort() 总是会中断之前的 XHR 请求。数据库中的数据很好,但有时它存储的是以前的版本。

我知道这是因为 PHP 没有停止执行,即使客户端已经中止,有时前一个请求比上一个请求花费了更多时间并且在最后一个请求之后完成,所以我阅读了函数手册“ignore_user_abort”和“connection_aborted”,但问题仍然存在。

我创建了这个脚本来模拟这种情况,我希望当我中止连接时(按“停止”,关闭选项卡/窗口),数据库上没有任何新数据,但 5 秒后,我仍然有新数据,所以当用户中止连接时我需要帮助来回滚事务。

这是要模拟的脚本(定义了 PDO_DSN、PDO_USER、PDO_PASS):

<?php
ignore_user_abort(true);

ob_start('ob_gzhandler');

$PDO = new PDO(PDO_DSN, PDO_USER, PDO_PASS, array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));

$PDO->beginTransaction();
$query = $PDO->query('INSERT INTO `table` (`content`) VALUES (' . $PDO->quote('test') . ')');
sleep(5);
echo ' ';
ob_flush();
flush();
if (connection_aborted()) {
  $PDO->rollBack();
  exit;
}
$PDO->commit();

ob_end_flush();

【问题讨论】:

  • 如果设置ignore_user_abort(false)会怎样?
  • @ExplosionPills 问题依然存在,connection_aborted() 无法与 ignore_user_abort(false) 一起使用
  • 在您尝试执行输出之前,php 无法可靠地检测连接是否/何时中止。见注释:php.net/manual/en/function.ignore-user-abort.php
  • @MarcB 我执行了输出,见代码的第 11 行。
  • 问题是$PDO = new PDO...为当前的PHP脚本创建了一个连接。您不能从 另一个 PHP 脚本中断当前执行。您可以使用一个脚本将 tmp 文件写入磁盘,然后检查 tmp 文件以及是否缺少回滚。任何脚本都可以删除 tmp 文件。

标签: php mysql ajax rollback abort


【解决方案1】:

如果您发现 XHR.abort()connection_aborted() 不可靠,请考虑发送带外信号以通知正在运行的 PHP 请求它不应提交事务的其他方法。

你在运行APC(或者你可以)吗?

您可以发送另一个 XHR 请求以发出中止信号,而不是调用 XHR.abort()。此请求的目的是在 APC 用户缓存中记录一个特殊键。该键的存在将向正在运行的 PHP 请求表明它应该回滚。

为了完成这项工作,每个 XHR 请求都需要携带一个(相对)唯一的事务标识符,例如作为表单变量。该标识符将随机生成,或根据当前时间生成,并将在初始 XHR 以及“中止”XHR 中发送,并允许中止请求与正在运行的请求相关联。在下面的示例中,事务标识符在表单变量t 中。

“中止”XHR 处理程序示例:

<?php
$uniqueTransactionId = $_REQUEST['t'];
$abortApcKey = 'abortTrans_' . $uniqueTransactionId;

apc_store($uniqueTransactionId, 1, 15);

修改数据库写入 XHR 处理程序示例:

<?php
$PDO = new PDO(PDO_DSN, PDO_USER, PDO_PASS,
               array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));

$PDO->beginTransaction();
$query = $PDO->query('INSERT INTO `table` (`content`) VALUES (' . $PDO->quote('test') . ')');

$uniqueTransactionId = $_REQUEST['t'];
$abortApcKey = 'abortTrans_' . $uniqueTransactionId;
if (apc_exists($abortApcKey)) {
  $PDO->rollBack();
  exit;
}

$PDO->commit();

您可能仍有时间问题。中止可能仍然来得太晚而无法停止提交。为了优雅地处理这个问题,您可以修改数据库写入处理程序以记录一个 APC 键,指示事务已提交。然后,中止处理程序可以检查此密钥是否存在,并返回一个有意义的 XHR 中止结果以通知客户端:“抱歉,我来晚了。”

请记住,如果您的应用程序托管在多个实时服务器上,您将需要使用共享缓存,例如 memcachedredis,因为 APC 的缓存仅在单台计算机上的进程之间共享。

【讨论】:

  • +1 有趣的想法,您使用了相同的原理,但不是调用connection_aborted(),而是使用另一个信号来回滚更改,但是使用其他额外请求仍然存在时间问题,iWantSimpleLife 的想法是最接近解决方案,但是我将为您的广泛而完整的答案提供 100 名声望。
【解决方案2】:

让浏览器发回您也存储在数据库中的时间戳或运行编号怎么样。并且您的更新可以检查,以便它仅在新时间戳较新时写入。

【讨论】:

  • +1 感谢您的回复,您的想法与我阅读 Evan 的回答后得到的想法相似。 :)
【解决方案3】:

我在 Javascript 和 Ajax 中多次看到这个问题。如果您在实现 UI 时不是很小心,那么用户可以单击两次和/或浏览器可以触发两次 ajax 调用,从而导致同一条记录两次访问您的数据库。这可能是来自您的 UI 的两个完全不同的 http 请求,因此您需要确保您的服务器端代码可以在将重复项插入数据库之前过滤掉它们。 我的解决方法通常是查询同一用户最近输入的记录,并检查这是否真的是一个新条目。如果这个新记录还没有在数据库中,则插入它,否则忽略。 在 Oracle 中,您可以使用 PL/SQL 来执行 MERGE IF MATCH THEN INSERT 命令,因此您可以在一个查询中处理此问题,但在 MySQL 中,您最好使用两个查询 - 一个查询该用户的现有记录,然后如果没有匹配,则插入另一个。

【讨论】:

    【解决方案4】:

    正如 Marc B 所指出的,如果不向浏览器发送一些输出,PHP 无法检测浏览器是否断开连接。问题是,您在ob_start('ob_gzhandler'); 行启用了输出缓冲,这会阻止PHP 将输出发送到浏览器。

    您必须删除该行或添加 ob_end_*(例如:ob_end_flush())以及 echo/flush 调用。

    【讨论】:

    • 已经尝试不使用ob_start() 并且在echo 一些文本之后仅使用flush(),但仍然没有工作。
    • ob_flush() 也会刷新输出缓冲区,如果它设置在您的 php.ini 上 - 您是否尝试过使用 ini_set('output_buffering', false);both flush() 和 @ 987654329@(即使没有ob_start())?
    • 是的,我尝试使用 ini_set('output_buffering', false);ini_set('output_buffering', '1'); 没有成功。
    猜你喜欢
    • 2015-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-21
    • 1970-01-01
    相关资源
    最近更新 更多