Предотвращение одновременных транзакций в веб-приложении

У нас есть веб-приложение (это игра) с множеством различных форм и элементов, которые действуют как кнопки и запускают какие-то действия на сервере. Проблема в том, что иногда пользователи могут запутаться в нашем приложении, если он слишком быстро нажимает на кнопки или открывает сайт в двух вкладках, а затем выполняет какие-то действия одновременно. У нас есть некоторая базовая защита — транзакции MySQL, некоторые двойные щелчки, предотвращающие Javascript, но в любом случае иногда что-то просто пропускает. Конечно, лучше всего было бы перепроектировать все SQL-транзакции и вспомогательные функции таким образом, чтобы не запутать систему. Одним из примеров такой путаницы является одновременная выдача двух обновлений: один веб-запрос изменяет что-то в базе данных, но второй запрос все еще работает со старыми данными, и поэтому обновление SQL возвращает «количество затронутых строк равно нулю», потому что первая транзакция уже изменилась. данные в БД. Очевидное решение состоит в том, чтобы прочитать данные еще раз прямо перед ОБНОВЛЕНИЕМ, чтобы увидеть, нуждаются ли они в обновлении, но это означает, что нужно помещать гораздо больше двойных запросов SELECT везде, а не хорошее решение - зачем читать одни и те же данные из БД дважды? Кроме того, мы рассмотрели возможность использования некоторого скрытого токена для сравнения на сервере при каждом запросе на обновление и запрете операций, которые имеют один и тот же идентификатор токена, но это также означает касание очень многих мест кода и, возможно, внесение новых ошибок в систему, которая работает отлично, за исключением это одна проблема.

Логика потока действий будет следующей: если пользователь выдал два запроса одновременно, второй запрос должен дождаться завершения первого. Но мы также должны учитывать, что в нашем приложении есть много перенаправлений (например, после POST, чтобы избежать двойного POST, когда пользователь обновляет страницу), поэтому решение не должно создавать взаимоблокировки для пользователя.

Итак, вопрос: что было бы самым простым из возможных глобальных исправлений, которые могли бы каким-то образом сделать все пользовательские операции последовательными? Есть ли хорошее универсальное решение?

Мы используем MySQL и PHP.


person JustAMartin    schedule 23.09.2010    source источник


Ответы (3)


Я рад, что вы понимаете, что это всего лишь плохое и временное решение, и вам следует оптимизировать свой код. Забудьте о своих жетонах и прочем. Самый простой и все же эффективный способ, вероятно, состоит в том, чтобы иметь эксклюзивную блокировку файла для общего файла для каждой операции. Вы можете представить это как кусок дерева, и только один может владеть id, и только тот, кто держит его, может говорить или что-то делать.

<?php
    $fp = fopen("/tmp/only-one-bed-available.txt", "w+");

    if (flock($fp, LOCK_EX)) { // do an exclusive lock

         // do some very important critical stuff here which must not be interrupted:
         // sleeping.
         sleep(60);
         echo "I now slept 60 seconds";
        flock($fp, LOCK_UN); // release the lock
    } else {
        echo "Couldn't get the lock!";
    }

    fclose($fp);
?>

Если вы выполните этот скрипт 10 раз параллельно, это займет 10 минут (потому что доступна только одна кровать!) и вы увидите эхо "Я сейчас спал..." каждые 60 секунд (приблизительно).

Это сериализует ВСЕ выполнения этого кода ГЛОБАЛЬНО. Это, вероятно, не то, что вы хотите (вы хотите это для каждого пользователя, не так ли?) Я уверен, что у вас есть что-то вроде идентификаторов пользователей, в противном случае используйте иностранный IP-адрес и уникальное имя файла для каждого пользователя :

<?php $fp = fopen("/tmp/lock-".$_SERVER["REMOTE_ADDR"].".txt", "w+"); ?>

Вы также можете определить имя файла блокировки для группы операций. Может быть, пользователь может делать что-то параллельно, но только эта операция создает проблемы? Дайте ему тег и включите его в имя файла блокировки!

Эта практика действительно не так уж плоха и может быть использована! Есть только несколько способов сделать это быстрее (внутренние компоненты mysql, сегменты общей памяти, сериализация на более высоком уровне, например балансировщик нагрузки...)

С решением, опубликованным выше, вы можете сделать это в своем приложении, что, вероятно, хорошо. Вы можете использовать ту же схему и в Mysql: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock

Но реализация PHP, вероятно, проще и лучше для вашего варианта использования.

Если вы действительно хотите надрать задницу еще более элегантному решению, просто проверьте эту функцию http://www.php.net/manual/en/function.sem-get.php

person The Surrican    schedule 23.09.2010
comment
PS: $_SERVER[REMOTE_ADDR] может быть не уникальным, потому что многие IP-адреса находятся за маршрутизаторами. Я бы предпочел вызвать uniqid() и вместо этого сохранить значение внутри файла cookie пользователя. - person Alfred; 24.09.2010
comment
лол, правда, Альфред, но есть много пользователей за маршрутизаторами, которые не принимают файлы cookie, используют один и тот же пользовательский агент, отключают javascript и имеют те же настройки браузера: D, что насчет них? :D ... но ты прав. комбинация может быть хорошей. - person The Surrican; 24.09.2010

Две вкладки и одновременное действие? Насколько быстры ваши пользователи?

В любом случае: разрешение только одной вкладки уже было бы хорошим решением.

С помощью только одной вкладки вы можете складывать действия в javascript и отправлять новые, как только вы получите возвращаемое значение из последнего вызова.

person Loïc Février    schedule 23.09.2010
comment
Возможно, пользователи не такие быстрые, но наш сервер иногда может немного тормозить :D Так что, если пользователь нажмет «Продать» для одного и того же игрового предмета на обеих вкладках или на одной и той же вкладке дважды, он может получить свои игровые деньги обратно дважды. - person JustAMartin; 23.09.2010
comment
Почему бы вам не проверить на стороне сервера, что пользователь может выполнить действие? Проверьте это, а затем сделайте это. А в javascript предотвратите двойной щелчок: после того, как пользователь щелкнул, выполните действие javascript (переместите объект, удалите его), а затем отправьте запрос. Если вы выполняете проверку в javascript, вы уверены, что действие будет действительным. Но только в javascript нет способа решить проблему с двумя вкладками. - person Loïc Février; 23.09.2010
comment
Проблема с некоторыми длительными операциями. Если пользователь выдает второй запрос в тот момент, когда первый запрос после SELECT, но до обновления, то у меня есть следующее: SELECT(1) SELECT(2) UPDATE(1), UPDATE(2) - и обновление 2 неверно потому что он был выполнен на старых данных, игнорируя изменения после UPADTE(1). - person JustAMartin; 23.09.2010
comment
Вот почему транзакции и блокировки созданы для: если вы собираетесь изменить материал, добавьте блокировку, выполните выбор/обновление, а затем снимите блокировку. - person Loïc Février; 23.09.2010

Как насчет использования функции mysql get_lock? Также, возможно, вам следует взглянуть на транзакции mysql.

person Alfred    schedule 23.09.2010