GetInfo.Ru – Компьютерная библиотека
Последние поступления
Как выбрать систему управления базы данных
Базы данных03/09/14
Этапы загрузки UNIX (в схеме)
Unix27/03/12
Gatewall Antispam: тотальный контроль электронной почты
Спам21/04/11
Мастер-класс: создаем Интернет-магазин (Часть 1)
Обзоры ПО20/04/11
CorelDRAW Graphics Suite X5: Что нового?
Обзоры ПО20/07/10
Добавить статью
Самые читаемые материалы
PHP: авторизация доступа(82594)
Как сделать Интернет-магазин?(65370)
Сессии в PHP(54088)
Веб-сайт: сборка по кирпичикам(29414)
СЕССИИ - обучение и /правильное/ использование(19397)
Всего статей: 793Всего авторов: 364Подразделов: 47Добавлено за сутки: 0
Статьи  СТАТЬИ Форум  ФОРУМ Рейтинг  РЕЙТИНГ Поиск  ПОИСК Контакты  КОНТАКТЫ
» Главная » PHP » Параллельное выполнение скриптов может привести к ошибке в базе данных

Параллельное выполнение скриптов может привести к ошибке в базе данных


Дмитрий Бородин
dima@php.spb.ru
http://php.spb.ru/

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

Предствьте, нам надо решить некую задачу, которая свелась к следующему алгоритму:

  • есть таблица mytest с полями a и b (это 2 переменные)
  • в таблице только одна строка, изначально в поле a записано число нуль, в поле b некоторое число, нам не известное
  • при возникновении команды от пользователя (человек нажал кнопку SUBMIT или при любом другом событии) надо проверить, равно ли поле a нулю, и если да, то записать в a единицу и увеличить поле b на единицу
Обратите внимание:
  • число b надо "УВЕЛИЧИТЬ НА", а не записать туда что-то
  • нельзя брать заранее значение b, т.к. данный простейший алгоритм считает это лишней нагрузкой (на счет этого пункта в конце)
  • если a не равно нулю, не надо ничего делать
  • другими словами, надо сделать программу, работающую в точном соответствии с описанием
Для тех, кто не понял, что же это за простейший алгоритм, объясняем его другими словами: 1) взять $a и $b из базы данных 2) если $a равно нулю, то записать в базу данных в переменную $a число 1 и увеличить $b на единицу.

Какие же проблемы могут возникнуть?
При запуске 2-х или более параллельно работающих скриптов легко обнаружить, что число в поле b (или $b - в упрощенном примере), к сожалению, может неоднократно увеличиваться на единицу. Представьте: запустился скрипт 1 и проверил, что $a содержит нуль. В этот момент запустился скрипт 2 и тоже узнал, что $a (это уже будет отдельная переменная в отдельном процессе номер 2) тоже содержит нуль. После этого оба скрипта решают, что надо установить $a в единицу и увеличить $b на единицу. Таким образом, как и в разобранном примере со счетчиком, можно так запустить параллельно работающие скрипты, чтобы произошла ошибка - увеличение переменной $b более одного раза. А это противоречит нашему алгоритму, который требует, увеличивать $b только один раз.

Когда эта проблема может возникнуть? Да когда угодно. Здесь мы опишем ситуацию, когда человек нажимает в форме много раз кнопку SUBMIT (отправить форму). Скрипт с помощью нехитрого алгоритма должен этому противостоять. Т.е. если программа видит установленную переменную $a, то программа должна игнорировать действия пользователя и ничего не делать. А если некий флаг еще не установлен (переменная $a пока равна нулю), то программа что-то делает. В нашем случае - прибавляет к $b единицу.

Проверка данного факта
Верите ли вы, что все описанное действительно суровая правда, а не теория? Если в примере со счетчиком очень легко было убедиться в его проблемах, то тут это может показаться не очевиным. Поэтому проверим, что наша теория верна и напишем программу, отвечающую алгоритму.


<? /* Перед началом программы создайте таблицу mytest: CREATE TABLE mytest ( a tinyint(4) DEFAULT '0' NOT NULL, b tinyint(4) DEFAULT '0' NOT NULL ) и поместите туда одну строку с двумя нулями: INSERT INTO mytest VALUES ( '0', '0'); */ // следующие 4 параметра (хост, имя пользователя, пароль, база данных) // должны соответствовать вашим данным mysql_connect("127.0.0.1","<имя пользователя>","<пароль>") or die("can't open"); mysql_select_db("<база данных>") or die("can't select"); // переменная $с - текущая команда // всего 3 конанды: 1) "" (ничего) - вывести значение переменных // 2) "clear" - обнулить поля (чтобы в ручную не обнулять) // 3) "submit" - сама операция switch ($c) { case "": // ничего хитрого, просто выводим перменные на экран $res=mysql_query("SELECT * FROM mytest") or die("error 1"); $a=mysql_result($res,0,"a"); $b=mysql_result($res,0,"b"); echo "A=$a, B=$b   <a href=$PHP_SELF?c=clear>сбросить в 0</a> <form action=$PHP_SELF> <input type=hidden name=c value='submit'> <input type=submit> </form>"; break; case "clear": // сброс в нуль, если человек использует ссылку "СБРОСИТЬ В 0" $res=mysql_query("UPDATE mytest SET a=0,b=0") or die("error 2"); header("Location: $PHP_SELF"); break; case "submit": // оновная программа, демонстрирующая проблему параллельно работающих // скриптов // первая часть алгоритма - взять переменные $a и $b из базы данных $res=mysql_query("SELECT * FROM mytest") or die("error 3"); $a=mysql_result($res,0,"a"); // вторая часть алгоритма, если $a равно нулю.... if ($a==0) { // то обновить данные в таблице: в $a записать 1, к $b прибавить 1 sleep(5); $res=mysql_query("UPDATE mytest SET a=1,b=b+1") or die("error 2"); exit(header("Location: $PHP_SELF")); } else // если $a уже не равно нулю, то вывести сообщение, что // человек пытался нажать 2 раза на кнопку SUBMIT и, соотвественно, // 2 раза выполнился скрипт echo "Не обновлено, т.к. A не равно 0 (A=$a)."; break; } ?>

Запустите программу и нажмите на кнопку SUBMIT быстро пару раз. Через некоторое время, когда скрип закончит работу, на главной странице вы увидите, что в $a записана единица, а в $b число, большее единицы.

Для чего мы написали команду sleep(5), которая останавливает выполнения скрипта на 5 секунд? Специально, чтобы указать на узкое место в нашем алгоритме. Сервер - это не идеальное устройство. ПХП-процессов не идельный интерпретатор файлов: мы знаем, что скрипты обрабатываются не последовательно, а часто параллельно. Поэтому время между выполнением двух критичных команд может быть большим. И в это время могут отнять обработки других скриптов, в том числе и того ужасного, что поступает от пользователя, случайно нажавшего SUBMIT два раза... Или хакера, который теоретически может получить выгоду от того, что некоторые действия могут быть выполнены большое число раз, хотя алгоритм (в поставленной задаче и так, как хотел программист) требовал всего один раз.

Откуда взялась переменная $b и нельзя ли от нее избавиться?
Можно! Но не всегда. (А как работает ваша программа?) Если ваш алгоритм можно немного изменить, т.е. не УВЕЛИЧИВАТЬ $b НА ЕДИНИЦУ, а записывать в $b число, равное переменной $b и единицы - это решение проблемы. Но представьте, что у вас не такая простейшая задача. Представьте, что вам нужно рельно сделать все тоже самое, только делать не изменение переменной $b, а чего-нибуль более значительного (если человек обманет систему провеки на двойное нажатие):

  • разослать почту по куче адресов
  • в какой-то финансовой среде использовать бонус (в виде переменной $a) и прибавить к счету в банке ($b) определенную сумму (константа)
  • вывести на принтер фразу "Привет, мир!"

Итог демонстрирования проблемы
У вас возник вопрос, с чего взялась эта кнопка SUBMIT и форма? Посмотрите на исходную постановку алгоритма. Там сказано только о некотором СОБЫТИИ. Кнопа SUBMIT как нельзя более точно описывает, что такое собитие. Все знают и долгое время работают и с кнопками, и с формами, и сталкивались с проблемами повторных нажатий. Некоторые даже вставляли защиту от повторого нажатия. Но даже такая защита не верна, что мы подробно разобрали в примерах и программе.

Решение проблемы
Отвлекитесь от кнопок и форм. Если вы программист (а не человек, желающий побыстрее накатать скрипт и заработать деньги), вы долны подумать о всех тонких моментах работы скрипта. В часности, что будет при параллельном выполнении вашего скрипта. Теперь о главном. Мы описали простой алгоритм и решение у него тоже простое. Если вы храните ваши данные в базе данных и не хотите привлекать сюда посторонние предметы (создание файлов - флагов, использование расшаренной памяти, сессий или др) то поможет метод блокирования таблицы MySQL перед моментом чтения данных и до окончания записи в нее. Если вы читали пример со счетчиком, то для решения используется та же самая идея блокирования места, откуда поступают переменные.

Чтобы заблокировать таблицу от чтения и записи дополним программу командой LOCK TABLES имяТаблицы WRITE и после использования таблицы снимаем блокировку UNLOCK TABLES. Кусок модифицированной части программы:


case "submit": if ($lock) mysql_query("LOCK TABLES mytest WRITE") or die("error 4"); $res=mysql_query("SELECT * FROM mytest") or die("error 3"); $a=mysql_result($res,0,"a"); if ($a==0) { sleep(5); $res=mysql_query("UPDATE mytest SET a=1,b=b+1") or die("error 2"); mysql_query("UNLOCK TABLES") or die("error 5"); exit(header("Location: $PHP_SELF")); }

После этого запускаем программу и пробуем нажать несколько раз (быстро) на кнопку SUBMIT. Через некоторое время программа известит, что не смогла ничего обновить (т.к. $a уже не нуль). Вернее, программа сделает обновление b только 1 раз. А все остальные копии скрипта, которые будут приостановлены из-за блокировки таблицы на 5 секунд, ничего не исортят.

И на последок...
Разумеется, если вы постаратесь не использовать такого алгоритма, это будет решение, при котором не придется блокировать таблицы, приводящие к снижению скорости работы сервера.

 
08.02.2003
Версия для печати Версия для печати Запомнить ссылку Запомнить ссылку
Ваша оценка:  1   2   3   4   5     

 О проектеПерепечаткаАвторамПартнерыО нас пишут
Наверх
©2003—2007. GETINFO.RU. ВСЕ ПРАВА ЗАЩИЩЕНЫ.