Как обрабатывать ситуации взаимоблокировки MySQL на уровне приложений?

Когда в MySQL/InnoDB возникает тупиковая ситуация, она возвращает эту знакомую ошибку:

"Тупик обнаружен при попытке получить блокировку; попробуйте перезапустить транзакцию "

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

Проблема. Когда у вас есть запросы, зависящие от результатов предыдущих запросов, это не работает так хорошо.

Например:

START TRANSACTION;
INSERT INTO some_table ...;
-- Application here gets ID of thing inserted: $id = $database->LastInsertedID()
INSERT INTO some_other_table (id,data) VALUES ($id,'foo');
COMMIT;

В этой ситуации я не могу просто переиздать транзакцию, поскольку она была изначально создана. Идентификатор, полученный первым оператором SQL, больше недействителен после того, как транзакция завершилась неудачно, но используется вторым оператором. Между тем, многие объекты были заполнены данными из транзакции, которые затем становятся устаревшими, когда транзакция откатывается назад. Конечно, сам код приложения "откатывается" с базой данных.

Вопрос: как я могу справиться с этими ситуациями в коде приложения? (PHP)

Я принимаю две вещи. Скажите, пожалуйста, если вы думаете, что я на правильном пути:

1) Поскольку база данных не может просто переиздавать транзакцию дословно в любых ситуациях, мое исходное решение не работает и не должно использоваться.

2) Единственный хороший способ сделать это - обернуть любой и весь код выдачи транзакций в свой собственный блок try/catch и попытаться переиздать сам код, а не только SQL.

Спасибо за ваш вклад. Ты жжешь.

Ответ 1

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

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

Поскольку вы видели записи запросов и повторное воспроизведение их не является решением, как при перезагрузке транзакции база данных переместилась. Если бы это было правильное решение, SQL-движок наверняка сделает это за вас. Для меня правила:

  • повторите все ваши чтения внутри транзакций (любые данные, которые вы прочитали снаружи, возможно, были изменены)
  • бросить все от предыдущей попытки, если вы пишете вещи за пределами транзакции (журналы, LDAP, что-либо вне SGBD), ее следует отменить из-за отката
  • повторить все на самом деле :-)

Это означает цикл повтора.

Таким образом, у вас есть блок try/catch с транзакцией внутри. Вам нужно добавить while цикл с, может быть, 3 попытки, вы оставите время цикла, если совершить часть кода успеха. Если после 3 повторных попыток транзакция все еще терпит неудачу, запустите Exception для пользователя - чтобы вы не пытались использовать замкнутый цикл повтора, у вас может быть действительно большая проблема на самом деле. Обратите внимание, что вы должны обрабатывать SQL-ошибку и блокировать или сериализовать исключение по-разному. 3 - произвольное число, вы можете попробовать большее количество попыток.

Это может дать что-то вроде этого:

$retry=0;
$notdone=TRUE;
while( $notdone && $retry<3 ) {
  try {
    $transaction->begin();
    do_all_the_transaction_stuff();
    $transaction->commit();
    $notdone=FALSE;
  } catch( Exception $e ) {
    // here we could differentiate basic SQL errors and deadlock/serializable errors
    $transaction->rollback();
    undo_all_non_datatbase_stuff();
    $retry++;
  }
}
if( 3 == $retry ) {
  throw new Exception("Try later, sorry, too much guys other there, or it not your day.");
}

И это означает, что все вещи (читая, записывая, fonctionnal вещи) должны быть заключены в $do_all_the_transaction_stuff(); , Предполагая, что код управления транзакциями находится в контроллерах, основной код с высоким уровнем приложения, а не разделен на несколько объектов с низким уровнем доступа к базе данных.