Откат транзакции, когда пользователь прерывает соединение


Прежде всего, извините за мой английский, я написал это с помощью Google Translate.

Я пытаюсь создать приложение, подобное Google Wave, с помощью PHP и Ajax. У меня есть текстовая область, в которой, когда пользователь что-то вводит, javascript на странице обнаруживается с помощью oninput и отправляет содержимое текстовой области на сервер, а сервер сохраняет содержимое в базе данных.

Что я делаю, так это то, что каждый раз, когда я отправляю контент по 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();
Author: msatbab, 2013-02-15

4 answers

Как насчет того, чтобы браузер отправил обратно метку времени или рабочий номер, который вы также храните в базе данных. и ваше обновление может проверить, чтобы оно записывало только в том случае, если новая временная метка более новая.

 1
Author: iWantSimpleLife, 2013-02-20 01:33:45

Если вы считаете XHR.abort() и connection_aborted() ненадежными, рассмотрите другие способы отправки внеполосного сигнала, чтобы сообщить запущенному PHP-запросу, что он не должен совершать транзакцию.

Вы используете APC (или могли бы быть)?

Вместо вызова XHR.abort() вы можете отправить другой запрос XHR, сигнализирующий об отмене. Целью этого запроса будет запись специального ключа в кэш пользователя 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, чтобы сообщить клиенту: "Извините, я опоздал"

.

Продолжайте имейте в виду, что если ваше приложение размещено на нескольких живых серверах, вы захотите использовать общий кэш, такой как memcached или redis, поскольку кэш APC является общим только для процессов на одной машине.

 2
Author: Evan, 2013-02-20 01:22:41

Я много раз сталкивался с этой проблемой в Javascript и Ajax. Если вы не очень осторожны в реализации пользовательского интерфейса, то пользователь может дважды щелкнуть и/или браузер может дважды вызвать вызов ajax, в результате чего одна и та же запись дважды попадет в вашу базу данных. Это могут быть два совершенно разных http-запроса из вашего пользовательского интерфейса, поэтому вам нужно убедиться, что ваш код на стороне сервера может отфильтровывать дубликаты, прежде чем вставлять их в базу данных. Мое решение обычно заключается в том, чтобы запросить записи недавно введенный одним и тем же пользователем и проверьте, действительно ли это новая запись или нет. Если этой новой записи еще нет в базе данных, вставьте ее, в противном случае игнорируйте. В Oracle вы можете использовать PL/SQL для выполнения команды "ОБЪЕДИНИТЬ, ЕСЛИ СОВПАДЕНИЕ, а ЗАТЕМ ВСТАВИТЬ", поэтому вы можете выполнить это в одном запросе, но в MySQL вам лучше использовать два запроса - один для запроса существующих записей этого пользователя, а затем другой для вставки, если совпадения нет.

 0
Author: Zoltan Fedor, 2013-02-19 21:19:04

Как указал Марк Б, PHP не может определить, отключен ли браузер, без отправки некоторого вывода в браузер. Проблема в том, что вы включили буферизацию вывода в строке ob_start('ob_gzhandler');, и это не позволяет PHP отправлять вывод в браузер.

Вам нужно либо удалить эту строку, либо добавить ob_end_* (например: ob_end_flush()) вместе с вызовами echo/flush.

 0
Author: Capilé, 2013-02-22 18:04:25