Под капотом: как работают транзакции базы данных в Laravel

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

Транзакции с замыканиями

Поскольку вы, вероятно, уже знакомы с транзакциями, вы можете использовать замыкания в методе транзакции фасада БД, например:

DB::transaction(function () {
    DB::update('update users set votes = 1');
    DB::delete('delete from posts');
});

Используя этот метод, вам не нужно беспокоиться о запуске транзакции, фиксации и откате назад, если что-то пойдет не так, он делает все это автоматически.

Методы, связанные с транзакциями, находятся в трейте Illuminate/Database/Concerns/ManagesTransactions.php. Вы также можете передать второй аргумент методу DB::transaction, который представляет собой количество попыток повторить попытку транзакции при возникновении исключения или взаимоблокировки. Код проходит через заданное число попыток и запускает новую транзакцию.

public function transaction(Closure $callback, $attempts = 1)
{
    for ($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
        $this->beginTransaction();

Он будет пытаться выполнить функцию колбека, и, если какое-либо исключение происходит в колбеке, откатывает транзакцию. Он будет пытаться снова, пока не достигнет заданного количества попыток:

try {
    $callbackResult = $callback($this);
}

// If we catch an exception we'll rollback this transaction and try again if we
// are not out of attempts. If we are out of attempts we will just throw the
// exception back out and let the developer handle an uncaught exceptions.
catch (Throwable $e) {
    $this->handleTransactionException(
        $e, $currentAttempt, $attempts
    );

    continue;
}

Если все прошло нормально фиксирует транзакцию,

try {
    if ($this->transactions == 1) {
        $this->getPdo()->commit();

        optional($this->transactionsManager)->commit($this->getName());
    }

    $this->transactions = max(0, $this->transactions - 1);
} catch (Throwable $e) {
    $this->handleCommitTransactionException(
        $e, $currentAttempt, $attempts
    );

    continue;
}

возвращает результат колбека и запускает событие commited:

$this->fireConnectionEvent('committed');

return $callbackResult;

Использование замыканий-это самый простой и легкий способ использования транзакций в Laravel.

Ручная обработка транзакций

Laravel позволяет вам самостоятельно контролировать транзакцию.

  • Запускаем транзакцию с помощью DB::beginTransaction();
  • Если все прошло хорошо , то фиксируем с помощью DB:: commit();
  • Или откат в противном случае с DB::rollBack();

Это работает просто отлично, но что делать, если вы хотите использовать вложенные транзакции? Например , MySQL не поддерживает вложенные транзакции, но движок InnoDB поддерживает точки сохранения.

Laravel использует эту функцию (если она поддерживается). Поэтому, когда мы вызываем DB::beginTransaction (), он подсчитывает уровень вложенности транзакций. В соответствии с этим уровнем вложенности он либо создает новую транзакцию (в первый раз), либо создает точку сохранения.

if ($this->transactions == 0) {
    $this->reconnectIfMissingConnection();

    try {
        $this->getPdo()->beginTransaction();
    } catch (Throwable $e) {
        $this->handleBeginTransactionException($e);
    }
} elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
    $this->createSavepoint();
}

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

protected function performRollBack($toLevel)
{
    if ($toLevel == 0) {
        $this->getPdo()->rollBack();
    } elseif ($this->queryGrammar->supportsSavepoints()) {
        $this->getPdo()->exec(
            $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
        );
    }
}

И вот тут начинается самое сложное: DB::commit() действительно зафиксирует базу данных только тогда, когда счетчик равен 1. В противном случае он только уменьшает счетчик и запускает событие committed

public function commit()
{
    if ($this->transactions == 1) {
        $this->getPdo()->commit();

        optional($this->transactionsManager)->commit($this->getName());
    }

    $this->transactions = max(0, $this->transactions - 1);

    $this->fireConnectionEvent('committed');
}

Очевидно, что это нормально, но вам нужно убедиться, что для каждой запущенной транзакции вы вызываете либо commit, либо rollback. Особенно, когда это делается в цикле.

Пример, когда все идет не так

Итак, вот пример того, что вызвало головную боль в нашем случае (это псевдокод, но суть та же):

$products = Products::all()
foreach($products as $product){
    try { 
        DB::beginTransaction();

        if ($product->isExpensiveEnough()) {
            continue;
        }

        $product->price += 100;
        $product->save();
        DB::commit();
    }
    catch(Exception $e){
        DB::rollback();    
    }
}

В коде выше мы перебираем продукты и изменяем какие то атрибуты в соответствии с некоторыми условиями. В этом псевдокоде вы поднимаем цены на продукты.

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

Однако когда условия не выполняются, и мы продолжаем, на следующей итерации создается новая транзакция. Счетчик транзакций будет увеличен до 2. Следующая фиксация не будет выполнять фактическую фиксацию в базе данных, а просто уменьшит счетчик. На следующей итерации начнется новая транзакция, увеличится счетчик до 2 и т. д. Как только число методов beginTransaction() и commit()/rollback() get “выйдет из синхронизации”, следующие итерации не будут обновлять записи.

Это не смешно, когда это команда, которая работает в течение пары часов, а в конце не обновляет ничего, что должна.

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

Рейтинг
( 3 оценки, среднее 5 из 5 )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.