Warning: Cannot use a scalar value as an array in /home/admin/public_html/forum/include/fm.class.php on line 757

Warning: Invalid argument supplied for foreach() in /home/admin/public_html/forum/include/fm.class.php on line 770

Warning: Invalid argument supplied for foreach() in /home/admin/public_html/forum/topic.php on line 737
Форумы портала PHP.SU :: Блокировка выполнения транзакции до её завершения

 PHP.SU

Программирование на PHP, MySQL и другие веб-технологии
PHP.SU Портал     На главную страницу форума Главная     Помощь Помощь     Поиск Поиск     Поиск Яндекс Поиск Яндекс     Вакансии  Пользователи Пользователи


 Страниц (1): [1]   

> Без описания
gheka
Отправлено: 18 Ноября, 2016 - 23:43:12
Post Id



Частый гость


Покинул форум
Сообщений всего: 191
Дата рег-ции: Февр. 2011  


Помог: 2 раз(а)




Здравствуйте

Есть транзакция с выполнением нескольких запросов подряд.

Суть примера такая.
Есть таблица bank с полями
id
sum Сумма банка

тип таблиц InnoDB


Любой авторизованный пользователь нажимает на кнопку ПОЛУЧИТЬ БАНК и выполняется скрипт ниже

ПРИМЕР

PHP:
скопировать код в буфер обмена
  1.  
  2. try {
  3.         /* Начало транзакции, отключение автоматической фиксации */
  4.         $dbh->beginTransaction();
  5.        
  6.         // Статус 1 (открыт)
  7.         $sql = "SELECT `id`,`sum` FROM `bank` WHERE `status`=? LIMIT 0,1";
  8.         $stmt = $dbh->prepare ( $sql );
  9.         $stmt->execute ( array ( 1 ) );
  10.        
  11.         if ( $data = $stmt->fetchAll ( PDO::FETCH_NUM ) ) {
  12.                 $id_invoice = $rows [0];
  13.                 $sum = $rows [1];
  14.                
  15.                 $sql = "UPDATE `user` SET `balanse`=balanse+? WHERE `id`=?";
  16.                 $stmt = $dbh->prepare ( $sql );
  17.                 $stmt->execute ( array ( $sum, $id_user ) );
  18.                
  19.                 // ДРУГИЕ ДЕЙСТВИЕ
  20.                
  21.                 // Обновляем статус на 0 (закрыт)
  22.                 $sql = "UPDATE `bank` SET `status`=? WHERE `id`=?";
  23.                 $stmt = $dbh->prepare ( $sql );
  24.                 $stmt->execute ( array ( 0, $id_invoice ) );
  25.         }                              
  26.         /* Фиксация изменений */
  27.         $dbh->commit();
  28. }
  29.  
  30. catch ( Exception $e ) {
  31.  
  32.         $dbh -> rollBack ();
  33.         die ("Ошибка : " . $e -> getMessage ());
  34. }
  35.  
  36.  


Тоесть выполняется запрос в базу bank и вытаскивается первая попавшееся запись со СТАТУСОМ 1 (статус 1 - банк открыт, статус 0 - банк закрыт)

Сумма из этого банка зачисляется на счёт этого пользователя.
После чего банк закрывается Статус банка с определённый id обновляется на 0


ВОПРОС

Мне нужно защитить данную транзакцию от двойной записи.
тоесть если 2 пользователя одновременно нажмут на кнопку ПОЛУЧИТЬ БАНК то один и тот же банк может зачислится обоим пользователям.
В том случае если транзакция не успеет завершится и статус таблицы bank ещё не изменится на 0


Надеюсь я понятно объяснил что я хочу.
Подскажите каким образом мне это сделать?

Читал про уровни изоляции

READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

но как их применить так и не понял.

(Отредактировано автором: 18 Ноября, 2016 - 23:53:55)

 
 Top
Мелкий Супермодератор
Отправлено: 18 Ноября, 2016 - 23:54:57
Post Id



Активный участник


Покинул форум
Сообщений всего: 11926
Дата рег-ции: Июль 2009  
Откуда: Россия, Санкт-Петербург


Помог: 618 раз(а)




SELECT ... FOR UPDATE
Захватит эксклюзивную блокировку на строку, соответственно до завершения этой транзакции остальные update или select for update (а так же select for share) будут ждать завершения транзакции.


-----
PostgreSQL DBA
 
 Top
gheka
Отправлено: 19 Ноября, 2016 - 08:40:08
Post Id



Частый гость


Покинул форум
Сообщений всего: 191
Дата рег-ции: Февр. 2011  


Помог: 2 раз(а)




Мелкий пишет:
SELECT ... FOR UPDATE
Захватит эксклюзивную блокировку на строку, соответственно до завершения этой транзакции остальные update или select for update (а так же select for share) будут ждать завершения транзакции.



Ну в этом примере да. Но это просто тестовый пример. Может быть и другие варианты.

Вот пример где SELECT не задействован в транзакции

PHP:
скопировать код в буфер обмена
  1.  
  2. // Статус 1 (открыт)
  3. $sql = "SELECT `id`,`sum` FROM `bank` WHERE `status`=? LIMIT 0,1";
  4. $stmt = $dbh->prepare ( $sql );
  5. $stmt->execute ( array ( 1 ) );
  6.  
  7. if ( $rows = $stmt->fetch ( PDO::FETCH_NUM ) ) {
  8.         $id_invoice = $rows [0];
  9.         $sum = $rows [1];
  10.  
  11.         try {
  12.                         /* Начало транзакции, отключение автоматической фиксации */
  13.                         $dbh->beginTransaction();
  14.                                    
  15.                         $sql = "UPDATE `user` SET `balanse`=balanse+? WHERE `id`=?";
  16.                         $stmt = $dbh->prepare ( $sql );
  17.                         $stmt->execute ( array ( $sum, $id_user ) );
  18.                    
  19.                         // ДРУГИЕ ДЕЙСТВИЕ
  20.                    
  21.                         // Обновляем статус на 0 (закрыт)
  22.                         $sql = "UPDATE `bank` SET `status`=? WHERE `id`=?";
  23.                         $stmt = $dbh->prepare ( $sql );
  24.                         $stmt->execute ( array ( 0, $id_invoice ) );
  25.                            
  26.                         /* Фиксация изменений */
  27.                         $dbh->commit();
  28.         }
  29.          
  30.         catch ( Exception $e ) {
  31.          
  32.                         $dbh -> rollBack ();
  33.                         die ("Ошибка : " . $e -> getMessage ());
  34.         }
  35. }
  36.  



Как в этом случае?

(Отредактировано автором: 19 Ноября, 2016 - 08:41:18)

 
 Top
Мелкий Супермодератор
Отправлено: 19 Ноября, 2016 - 12:15:33
Post Id



Активный участник


Покинул форум
Сообщений всего: 11926
Дата рег-ции: Июль 2009  
Откуда: Россия, Санкт-Петербург


Помог: 618 раз(а)




gheka пишет:
Как в этом случае?

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


-----
PostgreSQL DBA
 
 Top
gheka
Отправлено: 19 Ноября, 2016 - 13:05:07
Post Id



Частый гость


Покинул форум
Сообщений всего: 191
Дата рег-ции: Февр. 2011  


Помог: 2 раз(а)




Мелкий пишет:
gheka пишет:
Как в этом случае?

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



К примеру есть страница такая.
В комментариях указаны этапы


PHP:
скопировать код в буфер обмена
  1.  
  2. // ------------ 1 ЭТАП ------------
  3. // Проверяем есть ли открытые банки
  4. $sql = "SELECT `id`,`sum` FROM `bank` WHERE `status`=? LIMIT 0,1";
  5. $stmt = $dbh->prepare ( $sql );
  6. $stmt->execute ( array ( 1 ) );
  7.  
  8. if ( $rows = $stmt->fetch ( PDO::FETCH_NUM ) ) {
  9.         // Если есть выводим кнопку ПОЛУЧИТЬ БАНК
  10.         $id = $rows [0]; // К промеру ID 100
  11.         $sum = $rows [1]; // К примеру сумма 500
  12.  
  13.  
  14.  
  15.         // ВЫВОДИМ КНОПКУ
  16.                 echo "<form action='' method='post'>
  17.                 <input type='submit' name='receive_bonus' value='ПОЛУЧИТЬ БАНК'>
  18.                 </form>";
  19.                
  20.                
  21.                
  22.                 // ------------ 2 ЭТАП ------------
  23.                 // Проверим нажал ли пользователь кнопку
  24.                 if ( $_POST ['receive_bonus'] == TRUE ) {
  25.                
  26.                         try {
  27.                                   /* Начало транзакции, отключение автоматической фиксации */
  28.                                   $dbh->beginTransaction();
  29.                                                          
  30.                                   // ------------ 3 ЭТАП ------------
  31.                                   // Пополняем баланс пользователя который нажал кнопу
  32.                                   $sql = "UPDATE `user` SET `balanse`=balanse+? WHERE `id`=?";
  33.                                   $stmt = $dbh->prepare ( $sql );
  34.                                   $stmt->execute ( array ( $sum, $id_user ) );
  35.                          
  36.                                  
  37.                          
  38.                                   // ------------ 4 ЭТАП ----------------------
  39.                                   // Обновляем статус на 0 (банк закрыт)
  40.                                   $sql = "UPDATE `bank` SET `status`=? WHERE `id`=?";
  41.                                   $stmt = $dbh->prepare ( $sql );
  42.                                   $stmt->execute ( array ( 0, $id ) );
  43.                                          
  44.                                   /* Фиксация изменений */
  45.                                   $dbh->commit();
  46.                         }
  47.                          
  48.                         catch ( Exception $e ) {
  49.                          
  50.                                 $dbh -> rollBack ();
  51.                                 die ("Ошибка : " . $e -> getMessage ());
  52.                         }
  53.                 }
  54. } else {
  55.         echo "НЕТ ОТКРЫТЫХ БАНКОВ";
  56. }
  57.  


И Если допустим 2 пользователя зашли на страницу получения банка
Тоесть выполнился 1 этап

Они одновременно нажали на кнопку ПОЛУЧИТЬ БАНК

SELTCT выполнился и пустил обоих на 2-3 этап (Так как банк ещё открыт)

то обоим пользователям зачислится на баланс 500 руб

И только после этого выполнится 4 этап обновится статус банка на 0 (закрыт)
 
 Top
Мелкий Супермодератор
Отправлено: 19 Ноября, 2016 - 13:55:24
Post Id



Активный участник


Покинул форум
Сообщений всего: 11926
Дата рег-ции: Июль 2009  
Откуда: Россия, Санкт-Петербург


Помог: 618 раз(а)




Всё верно. Классика race condition.
Изначальный код в первом сообщении, где select уже в транзакции, но не захватывает эксклюзивную блокировку, тоже подвержен этому же race condition.

Третий раз написать, что для исключения состояния гонки в этом коде необходимо использовать select for update внутри транзакции? Только в этом случае СУБД нормально сериализует происходящее.

Этот кусок кода примечателен следующими штуками:
- речь о mysql, что видно по стилю оформления имён объектов в БД
- выбирается любой bank.status=1, а не какой-то конкретный. Значит при конкурентном доступе для второй транзакции может найтись другая подходящая строка после фиксации первой транзакции. В принципе, это условие даже можно сериализовать иным образом чем select for update, но эффективно получится то же самое при большем объёме кода и нескольких бесполезных транзакциях.

gheka пишет:
// Проверим нажал ли пользователь кнопку

Проверка реализована некорректно, к слову. submit формы может происходить без отправки input type=submit. Это допустимое поведение клиента.


-----
PostgreSQL DBA
 
 Top
gheka
Отправлено: 20 Ноября, 2016 - 13:55:18
Post Id



Частый гость


Покинул форум
Сообщений всего: 191
Дата рег-ции: Февр. 2011  


Помог: 2 раз(а)




Спасибо, да действительно то что нужно SELECT FOR UPDATE.
 
 Top
Страниц (1): [1]
Сейчас эту тему просматривают: 0 (гостей: 0, зарегистрированных: 0)
« Вопросы новичков »


Все гости форума могут просматривать этот раздел.
Только зарегистрированные пользователи могут создавать новые темы в этом разделе.
Только зарегистрированные пользователи могут отвечать на сообщения в этом разделе.
 



Powered by PHP  Powered By MySQL  Powered by Nginx  Valid CSS  RSS

 
Powered by ExBB FM 1.0 RC1. InvisionExBB