Основы PHP
  Что такое PHP?
  Возможности PHP
  Преимущества PHP
  История развития
  Что нового в PHP5?
  «Движок» PHP
  Переход на PHP 5.3
New Переход на PHP 5.6
  Введение в PHP
  Изучение PHP
  Основы CGI
  Синтаксис PHP
  Типы данных PHP
  Переменные в PHP
  Константы PHP
  Выражения PHP
  Операторы PHP
  Конструкции PHP
  Ссылки в PHP
  PHP и ООП
  Безопасность
  Функции PHP
  Функции по категориям
  Функции по алфавиту
  Стандартные функции
  Пользовательские
  PHP и HTTP
  Работа с формами
  PHP и Upload
  PHP и Cookies
  PHP и базы данных
  PHP и MySQL
  Документация MySQL
  Учебники
  Учебники по PHP
  Учебники по MySQL
  Другие учебники
  Уроки PHP
  Введение
  Самые основы
  Управление
  Функции
  Документация
  Математика
  Файлы
  Основы SQL
  Дата и время
  CURL
  Изображения
  Стили
  Безопасность
  Установка
  Проектирование БД
  Регулярные выражения
  Подготовка к работе
  Быстрый старт
  Установка PHP
  Установка MySQL
  Конфигурация PHP
  Download / Скачать
  Скачать Apache
  Скачать PHP
  Скачать PECL
  Скачать PEAR
  Скачать MySQL
  Редакторы PHP
  Полезные утилиты
  Документация
  PHP скрипты
  Скачать скрипты
  Инструменты
  PHP в примерах
  Новости портала
 Главная   »  Сборник статей
 
 

Ошибки БД. Параллельное выполнение скриптов

Автор: Дмитрий Бородин

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

См. также:


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

  • есть таблица 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 (
     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 &nbsp; <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 секунд, ничего не испортят.

И на последок...

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

 
 » Обсудить эту статью на форуме

 
 Сборник статей 
 Содержание раздела 
Есть еще вопросы или что-то непонятно - добро пожаловать на наш  форум портала PHP.SU 
 

 
Powered by PHP  Powered By MySQL  Powered by Nginx  Valid CSS