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
Форумы портала PHP.SU :: Версия для печати :: Блокировка выполнения транзакции до её завершения
Форумы портала PHP.SU » » Вопросы новичков » Блокировка выполнения транзакции до её завершения

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

1. gheka - 18 Ноября, 2016 - 23:43:12 - перейти к сообщению
Здравствуйте

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

Суть примера такая.
Есть таблица 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

но как их применить так и не понял.
2. Мелкий - 18 Ноября, 2016 - 23:54:57 - перейти к сообщению
SELECT ... FOR UPDATE
Захватит эксклюзивную блокировку на строку, соответственно до завершения этой транзакции остальные update или select for update (а так же select for share) будут ждать завершения транзакции.
3. gheka - 19 Ноября, 2016 - 08:40:08 - перейти к сообщению
Мелкий пишет:
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.  



Как в этом случае?
4. Мелкий - 19 Ноября, 2016 - 12:15:33 - перейти к сообщению
gheka пишет:
Как в этом случае?

Вы обязаны перенести запрос, на основании данных которого определяется дальнейшая логика в транзакцию и захватывать необходимые блокировки.
Т.е. этот пример идентичен предыдущему и безопасный для конкурентного доступа код будет идентичен.
5. gheka - 19 Ноября, 2016 - 13:05:07 - перейти к сообщению
Мелкий пишет:
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 (закрыт)
6. Мелкий - 19 Ноября, 2016 - 13:55:24 - перейти к сообщению
Всё верно. Классика race condition.
Изначальный код в первом сообщении, где select уже в транзакции, но не захватывает эксклюзивную блокировку, тоже подвержен этому же race condition.

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

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

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

Проверка реализована некорректно, к слову. submit формы может происходить без отправки input type=submit. Это допустимое поведение клиента.
7. gheka - 20 Ноября, 2016 - 13:55:18 - перейти к сообщению
Спасибо, да действительно то что нужно SELECT FOR UPDATE.

 

Powered by ExBB FM 1.0 RC1