Ошибки БД. Параллельное выполнение скриптов
Автор:Дмитрий Бородин
Здесь рассматривается вопрос, что бывает, если запустить некий скрипт почти одновременно (что происходит, например, при большой нагруженности сервера) несколько раз, т.е. запустить несколько копий одного и того же скрипта. При некотором описанном стечении обстоятельств это приводит к нарушению целостности базы данных (короче говоря - можно существенно подпортить ваш блестящий алгоритм и программу).
См. также:
См. также:
- Ошибки в файлах. Параллельное выполнение скриптов
Предствьте, нам надо решить некую задачу, которая свелась к следующему алгоритму:
- есть таблица 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 единицу.
Проверка данного факта
Верите ли вы, что все описанное действительно суровая правда, а не теория? Если в примере со счетчиком очень легко было убедиться в его проблемах, то тут это может показаться не очевиным. Поэтому проверим, что наша теория верна и напишем программу, отвечающую алгоритму.
<? PHP /* Перед началом программы создайте таблицу mytest: CREATE TABLE mytest ( tinyint (4) ПО УМОЛЧАНИЮ '0' NOT NULL, b tinyint (4) ПО УМОЛЧАНИЮ '0' НЕ ПУСТО ) и поместите туда одну строку с двумя нулями: INSERT INTO mytest VALUES ('0', '0'); */ // следующие 4 параметра (хост, имя пользователя, пароль, база данных) // должны соответствовать вашим данным mysql_connect ("127.0.0.1", "<имя пользователя>", "<пароль>") или die ("не могу открыть"); mysql_select_db ("<база данных>") или die ("не могу выбрать"); // переменная $с - текущая команда // всего 3 конанды: 1) "" (ничего) - вывести значение переменных // 2) "clear" - обнулить поля (чтобы в ручную не обнулять) // 3) "submit" - сама операция switch ($ c) { дело "": // ничего хитрого, просто выводим перменные на экран $ res = mysql_query ("SELECT * FROM mytest") или die ("ошибка 1"); $ А = mysql_result ($ разреш, 0, "а"); $ Ь = mysql_result ($ разреш, 0, "б"); echo "A = $ a, B = $ b & nbsp; <a href=$PHP_SELF?c=clear> сбросить в 0 </a> <form action = $ PHP_SELF> <input type = hidden name = c value = 'submit'> <input type = submit> </ Форма> "; перерыв; дело "ясно": // сброс в нуль, если человек использует ссылку "СБРОСИТЬ В 0" $ res = mysql_query («ОБНОВИТЬ mytest SET a = 0, b = 0») или die («ошибка 2»); header («Местоположение: $ PHP_SELF»); перерыв; дело "подать": // оновная программа, демонстрирующая проблему параллельно работающих // скриптов // первая часть алгоритма - взять переменные $a и $b из базы данных $ res = mysql_query («SELECT * FROM mytest») или die («ошибка 3»); $ А = mysql_result ($ разреш, 0, "а"); // вторая часть алгоритма, если $a равно нулю.... if ($ a == 0) { // то обновить данные в таблице: в $a записать 1, к $b прибавить 1 сна (5); $ res = mysql_query («ОБНОВИТЬ mytest SET a = 1, b = b + 1») или die («ошибка 2»); выход (заголовок («Местоположение: $ PHP_SELF»)); } еще // если $a уже не равно нулю, то вывести сообщение, что // человек пытался нажать 2 раза на кнопку SUBMIT и, соотвественно, // 2 раза выполнился скрипт echo "Не обновлено, т.к. A не равно 0 (A=$a)."; перерыв; } ?> |
Запустите программу и нажмите на кнопку SUBMIT быстро пару раз. Через некоторое время, когда скрип закончит работу, на главной странице вы увидите, что в $a записана единица, а в $b число, большее единицы.
Для чего мы написали команду sleep(5), которая останавливает выполнения скрипта на 5 секунд? Специально, чтобы указать на узкое место в нашем алгоритме. Сервер - это не идеальное устройство. ПХП-процессов не идельный интерпретатор файлов: мы знаем, что скрипты обрабатываются не последовательно, а часто параллельно. Поэтому время между выполнением двух критичных команд может быть большим. И в это время могут отнять обработки других скриптов, в том числе и того ужасного, что поступает от пользователя, случайно нажавшего SUBMIT два раза... Или хакера, который теоретически может получить выгоду от того, что некоторые действия могут быть выполнены большое число раз, хотя алгоритм (в поставленной задаче и так, как хотел программист) требовал всего один раз.
Для чего мы написали команду sleep(5), которая останавливает выполнения скрипта на 5 секунд? Специально, чтобы указать на узкое место в нашем алгоритме. Сервер - это не идеальное устройство. ПХП-процессов не идельный интерпретатор файлов: мы знаем, что скрипты обрабатываются не последовательно, а часто параллельно. Поэтому время между выполнением двух критичных команд может быть большим. И в это время могут отнять обработки других скриптов, в том числе и того ужасного, что поступает от пользователя, случайно нажавшего SUBMIT два раза... Или хакера, который теоретически может получить выгоду от того, что некоторые действия могут быть выполнены большое число раз, хотя алгоритм (в поставленной задаче и так, как хотел программист) требовал всего один раз.
Откуда взялась переменная $b и нельзя ли от нее избавиться?
Можно! Но не всегда. (А как работает ваша программа?) Если ваш алгоритм можно немного изменить, т.е. не УВЕЛИЧИВАТЬ $b НА ЕДИНИЦУ, а записывать в $b число, равное переменной $b и единицы - это решение проблемы. Но представьте, что у вас не такая простейшая задача. Представьте, что вам нужно рельно сделать все тоже самое, только делать не изменение переменной $b, а чего-нибуль более значительного (если человек обманет систему провеки на двойное нажатие):
- разослать почту по куче адресов
- в какой-то финансовой среде использовать бонус (в виде переменной $a) и прибавить к счету в банке ($b) определенную сумму (константа)
- вывести на принтер фразу "Привет, мир!"
Итог демонстрирования проблемы
У вас возник вопрос, с чего взялась эта кнопка SUBMIT и форма? Посмотрите на исходную постановку алгоритма. Там сказано только о некотором СОБЫТИИ. Кнопа SUBMIT как нельзя более точно описывает, что такое собитие. Все знают и долгое время работают и с кнопками, и с формами, и сталкивались с проблемами повторных нажатий. Некоторые даже вставляли защиту от повторого нажатия. Но даже такая защита не верна, что мы подробно разобрали в примерах и программе.
Решение проблемы
Отвлекитесь от кнопок и форм. Если вы программист (а не человек, желающий побыстрее накатать скрипт и заработать деньги), вы долны подумать о всех тонких моментах работы скрипта. В часности, что будет при параллельном выполнении вашего скрипта. Теперь о главном. Мы описали простой алгоритм и решение у него тоже простое. Если вы храните ваши данные в базе данных и не хотите привлекать сюда посторонние предметы (создание файлов - флагов, использование расшаренной памяти, сессий или др) то поможет метод блокирования таблицы MySQL перед моментом чтения данных и до окончания записи в нее. Если вы читали пример со счетчиком, то для решения используется та же самая идея блокирования места, откуда поступают переменные.
Чтобы заблокировать таблицу от чтения и записи дополним программу командой LOCK TABLES имяТаблицы WRITE и после использования таблицы снимаем блокировку UNLOCK TABLES. Кусок модифицированной части программы:
Чтобы заблокировать таблицу от чтения и записи дополним программу командой LOCK TABLES имяТаблицы WRITE и после использования таблицы снимаем блокировку UNLOCK TABLES. Кусок модифицированной части программы:
дело "подать": if ($ lock) mysql_query ("LOCK TABLES mytest WRITE") или die ("ошибка 4"); $ res = mysql_query («SELECT * FROM mytest») или die («ошибка 3»); $ А = mysql_result ($ разреш, 0, "а"); if ($ a == 0) { сна (5); $ res = mysql_query («ОБНОВИТЬ mytest SET a = 1, b = b + 1») или die («ошибка 2»); mysql_query ("UNLOCK TABLES") или die ("error 5"); выход (заголовок («Местоположение: $ PHP_SELF»)); } |
После этого запускаем программу и пробуем нажать несколько раз (быстро) на кнопку SUBMIT. Через некоторое время программа известит, что не смогла ничего обновить (т.к. $a уже не нуль). Вернее, программа сделает обновление bтолько 1 раз. А все остальные копии скрипта, которые будут приостановлены из-за блокировки таблицы на 5 секунд, ничего не испортят.
И на последок...
Разумеется, если вы постаратесь не использовать такого алгоритма, это будет решение, при котором не придется блокировать таблицы, приводящие к снижению скорости работы сервера. Еще, вы можете отказаться от использования базы данных и хранить важные флаги в сессиях.